Hone logo
Hone
Problems

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 createResponseResolver function should accept an optional defaultResponse object. This response will be used if no specific matching criteria are found.
  • The core of the configuration will be an array of resolverRules. Each resolverRule will have:
    • matchers: An array of functions. Each function in this array takes the arguments passed to the mocked function and should return true if the arguments match the criteria for this rule, and false otherwise.
    • response: The mock response object to return if all matchers for this rule return true.
  • The createResponseResolver function 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 the resolverRules.
  • The first resolverRule whose matchers all return true will have its response returned by the resolver function.
  • If no resolverRule matches, the defaultResponse (if provided) should be returned.
  • If no resolverRule matches and no defaultResponse is 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 resolverRules are provided.
  • No defaultResponse is provided.
  • Multiple resolverRules could potentially match. The resolver should use the first matching rule.
  • The matchers functions 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 matchers array within a resolverRule can be empty. If it is empty, that rule will always match.
  • The resolverRules array 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 createResponseResolver function should be efficient and not introduce significant overhead in your tests.

Notes

  • Consider how you will type the response and defaultResponse to be flexible. A generic type parameter for the response object might be beneficial.
  • The matchers functions 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.mock and mockImplementation.
  • The error message when no match is found should be informative, ideally including the arguments that were passed.
Loading editor...
typescript