Jest Mocking: Crafting a Flexible Response Resolver
This challenge focuses on creating a reusable utility function for Jest tests that can dynamically resolve mock responses based on the input. This is incredibly useful for testing asynchronous operations where the success or failure of a request can vary, allowing for more robust and predictable test scenarios.
Problem Description
You need to implement a TypeScript function called createResponseResolver. This function will act as a factory for creating mock response handlers for libraries like jest.mock or custom mocking utilities. The createResponseResolver should accept a configuration object that defines how to determine the appropriate mock response based on the arguments passed to the mocked function.
Key Requirements:
- The
createResponseResolverfunction should accept an optionaldefaultResponseobject. This response will be used if no specific matching criteria are found. - The core of the configuration will be an array of
resolverRules. EachresolverRulewill have:matchers: An array of functions. Each function in this array takes the arguments passed to the mocked function and should returntrueif the arguments match the criteria for this rule, andfalseotherwise.response: The mock response object to return if allmatchersfor this rule returntrue.
- The
createResponseResolverfunction should return a new function. This returned function will be the actual resolver. When called with the arguments of the mocked function, it should iterate through theresolverRules. - The first
resolverRulewhosematchersall returntruewill have itsresponsereturned by the resolver function. - If no
resolverRulematches, thedefaultResponse(if provided) should be returned. - If no
resolverRulematches and nodefaultResponseis provided, the resolver function should throw an error indicating that no matching response was found.
Expected Behavior:
The returned resolver function should behave like a flexible switchboard, directing the mock execution based on input.
Edge Cases:
- No
resolverRulesare provided. - No
defaultResponseis provided. - Multiple
resolverRulescould potentially match. The resolver should use the first matching rule. - The
matchersfunctions can be complex and involve checking specific properties, types, or values of the arguments.
Examples
Example 1: Basic Request Mocking
// Assume 'apiClient.getUser' is a mocked function
const getUserResolver = createResponseResolver({
defaultResponse: { status: 500, body: { message: 'Internal Server Error' } },
resolverRules: [
{
matchers: [
(id: string) => id === '123',
],
response: { status: 200, body: { id: '123', name: 'Alice' } },
},
{
matchers: [
(id: string) => id === '456',
],
response: { status: 200, body: { id: '456', name: 'Bob' } },
},
],
});
// In your Jest test:
// apiClient.getUser = jest.fn(getUserResolver);
// Calling apiClient.getUser('123') would resolve to { status: 200, body: { id: '123', name: 'Alice' } }
// Calling apiClient.getUser('789') would resolve to { status: 500, body: { message: 'Internal Server Error' } } (default)
Example 2: Mocking with Multiple Argument Checks
interface Product {
id: string;
category: string;
price: number;
}
// Assume 'apiClient.searchProducts' is a mocked function
const searchProductsResolver = createResponseResolver({
defaultResponse: { products: [] },
resolverRules: [
{
matchers: [
(query: string, options: { category?: string; minPrice?: number }) => query === 'laptop' && options.category === 'electronics',
],
response: { products: [{ id: 'p1', category: 'electronics', price: 1200 }] },
},
{
matchers: [
(query: string, options: { category?: string; minPrice?: number }) => query === 'book',
],
response: { products: [{ id: 'p2', category: 'books', price: 25 }] },
},
],
});
// In your Jest test:
// apiClient.searchProducts = jest.fn(searchProductsResolver);
// Calling apiClient.searchProducts('laptop', { category: 'electronics' }) would resolve to { products: [{ id: 'p1', category: 'electronics', price: 1200 }] }
// Calling apiClient.searchProducts('book', { minPrice: 10 }) would resolve to { products: [{ id: 'p2', category: 'books', price: 25 }] }
// Calling apiClient.searchProducts('keyboard') would resolve to { products: [] } (default)
Example 3: No Match and No Default
// Assume 'apiClient.deleteItem' is a mocked function
const deleteItemResolver = createResponseResolver({
resolverRules: [
{
matchers: [(id: string) => id === 'item-to-delete'],
response: { success: true },
},
],
});
// In your Jest test:
// apiClient.deleteItem = jest.fn(deleteItemResolver);
// Calling apiClient.deleteItem('some-other-id') should throw an error: "No matching response found for arguments: ['some-other-id']"
Constraints
- The
matchersarray within aresolverRulecan be empty. If it is empty, that rule will always match. - The
resolverRulesarray can be empty. - The arguments passed to the mocked function can be of any type and number. Your matcher functions should be able to handle these.
- The
createResponseResolverfunction should be efficient and not introduce significant overhead in your tests.
Notes
- Consider how you will type the
responseanddefaultResponseto be flexible. A generic type parameter for the response object might be beneficial. - The
matchersfunctions need to be able to inspect the arguments passed to the mocked function. The order of arguments matters. - Think about how you would integrate this into a larger mocking setup, for example, with
jest.mockandmockImplementation. - The error message when no match is found should be informative, ideally including the arguments that were passed.