Jest Custom Matcher: Implementing toReject
Jest is a powerful JavaScript testing framework that provides a rich set of built-in matchers. However, there are times when you need to assert specific behaviors related to asynchronous operations, particularly Promises that are expected to reject. This challenge focuses on creating a custom Jest matcher that specifically checks if a Promise rejects with a certain error or satisfies a condition.
Problem Description
Your task is to implement a custom Jest matcher named toReject. This matcher should be able to assert that a given Promise rejects. You will need to handle two primary scenarios:
- Rejecting with a specific error or value: The matcher should accept an optional argument, which can be an error instance, a regular expression, or a function. If provided, the matcher should verify that the rejected Promise's reason matches this argument.
- Rejecting without a specific argument: If no argument is provided, the matcher should simply assert that the Promise rejects, regardless of the rejection reason.
Key Requirements:
- The matcher should be named
toReject. - It must correctly handle Promises that resolve (should fail the assertion).
- It must correctly handle Promises that reject.
- If an argument is provided to
toReject:- If the argument is an
Errorinstance, the rejection reason must be an instance of that error. - If the argument is a
RegExp, the rejection reason'smessageproperty (if it exists and is a string) must match the regular expression. - If the argument is a function, this function will be called with the rejection reason. The matcher should pass if this function returns
true.
- If the argument is an
- The matcher should provide informative failure messages for both positive (
expect(promise).toReject()) and negative (expect(promise).not.toReject()) assertions. - The implementation should be in TypeScript.
Examples
Example 1: Basic Rejection
// Inside your test file:
const promiseThatRejects = Promise.reject(new Error('Something went wrong'));
test('should reject', async () => {
await expect(promiseThatRejects).toReject();
});
Example 2: Rejecting with a Specific Error Instance
// Inside your test file:
const specificError = new TypeError('Invalid type provided');
const promiseThatRejectsWithSpecificError = Promise.reject(specificError);
test('should reject with a specific error', async () => {
await expect(promiseThatRejectsWithSpecificError).toReject(TypeError);
});
Example 3: Rejecting with an Error Matching a RegExp
// Inside your test file:
const promiseThatRejectsWithSpecificMessage = Promise.reject(new Error('User not found. ID: 123'));
test('should reject with an error message matching a regex', async () => {
await expect(promiseThatRejectsWithSpecificMessage).toReject(/User not found/);
});
Example 4: Rejecting with a Custom Condition (Function)
// Inside your test file:
interface CustomError {
code: number;
message: string;
}
const promiseThatRejectsWithCustomError = Promise.reject<string, CustomError>({ code: 404, message: 'Resource not available' });
test('should reject with a custom error object satisfying a condition', async () => {
await expect(promiseThatRejectsWithCustomError).toReject((error) => {
if (typeof error === 'object' && error !== null && 'code' in error) {
const customError = error as CustomError;
return customError.code === 404;
}
return false;
});
});
Example 5: Promise Resolves (Failing Assertion)
// Inside your test file:
const promiseThatResolves = Promise.resolve('Success!');
test('should fail if promise resolves', async () => {
// This assertion should fail
// await expect(promiseThatResolves).toReject();
});
Constraints
- The implementation must be compatible with Jest v27+ and TypeScript.
- Your solution should be delivered as a Jest custom matcher.
- Avoid using external libraries for Promise manipulation beyond standard JavaScript.
Notes
- Remember that Jest custom matchers are asynchronous by nature when dealing with Promises. You'll likely need to use
async/awaitin your matcher implementation. - Consider how to properly handle the rejection reason. It could be an
Errorobject, a primitive value, or a complex object. - Think about how to construct clear and helpful failure messages. These messages are crucial for debugging failing tests.
- You will need to declare the custom matcher type for TypeScript to recognize it. You can typically do this by extending
JestMatchers<T>orGlobalinterfaces.