Hone logo
Hone
Problems

Jest's toThrow Matcher: Testing for Expected Errors

When writing robust software, it's crucial to not only ensure that your functions work correctly under normal circumstances but also that they handle errors gracefully. Jest's toThrow matcher is a powerful tool for verifying that a specific piece of code throws an error when expected. This challenge will guide you through implementing a custom Jest matcher that mimics the behavior of toThrow.

Problem Description

Your task is to create a custom Jest matcher, named toThrowError, that asserts whether a given function throws an error. This matcher should be versatile enough to:

  1. Detect if any error is thrown: The basic functionality is to check if the provided function, when executed, results in an error being thrown.
  2. Detect if a specific error type is thrown: The matcher should allow specifying an expected error constructor (e.g., TypeError, RangeError) and verify that the thrown error is an instance of that type.
  3. Detect if a specific error message is thrown: The matcher should allow specifying a string or a regular expression to match against the error's message.

You will need to define the toHave and toThrowError matchers within a Jest custom matcher setup.

Examples

Example 1: Basic Error Throwing

Consider a function that is designed to throw an error under certain conditions:

function divideByZero(a: number, b: number): number {
  if (b === 0) {
    throw new Error("Division by zero is not allowed.");
  }
  return a / b;
}

Your toThrowError matcher should be used as follows:

expect(() => divideByZero(10, 0)).toThrowError();

Example 2: Throwing a Specific Error Type

Now, let's refine the previous example to throw a specific error type:

class CustomError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "CustomError";
  }
}

function processData(data: any): void {
  if (typeof data !== 'string') {
    throw new TypeError("Input must be a string.");
  }
  // ... processing logic
}

Your toThrowError matcher should support checking the error type:

expect(() => processData(123)).toThrowError(TypeError);

Example 3: Throwing with a Specific Message (String)

Continuing with the divideByZero example, you might want to assert the exact error message:

function divideByZero(a: number, b: number): number {
  if (b === 0) {
    throw new Error("Division by zero is not allowed.");
  }
  return a / b;
}

Your toThrowError matcher should handle string message matching:

expect(() => divideByZero(10, 0)).toThrowError("Division by zero is not allowed.");

Example 4: Throwing with a Specific Message (Regex)

For more flexible message matching, a regular expression is useful:

function complexOperation(input: number): void {
  if (input < 0) {
    throw new Error("Invalid input: negative number provided. Expected non-negative.");
  }
  // ... complex logic
}

Your toThrowError matcher should support regex message matching:

expect(() => complexOperation(-5)).toThrowError(/Invalid input.*non-negative/);

Example 5: Combining Error Type and Message

The matcher should also allow specifying both the error type and a message pattern:

class SpecificError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "SpecificError";
  }
}

function performTask(value: number): void {
  if (value < 10) {
    throw new SpecificError("Value too low. Minimum required is 10.");
  }
  // ... task execution
}

Your toThrowError matcher should support this combination:

expect(() => performTask(5)).toThrowError(SpecificError, "Value too low.");

Constraints

  • Your custom matcher should be implemented using Jest's expect.extend API.
  • The matcher should be named toThrowError.
  • The matcher should accept optional arguments for the expected error type and/or message pattern.
  • The matcher should correctly handle cases where no error is thrown when one is expected.
  • The matcher should correctly handle cases where an error is thrown but it doesn't match the expected type or message.
  • The function passed to the matcher will always be a no-argument function or a function that can be invoked without arguments. For simplicity, assume it returns void.

Notes

  • You will need to import jest and define your custom matchers.
  • The asymmetricMatchers object in Jest can be useful for handling message matching with regular expressions.
  • Remember that Jest matchers should return an object with pass (boolean) and message (function) properties. The message function should return a string explaining why the assertion failed.
  • Consider the return value of the function being tested. For this challenge, we assume the function is invoked and we only care about whether it throws an error.
Loading editor...
typescript