Jest Test Utility: Mocking and Asserting API Calls
This challenge focuses on building robust test utilities for asynchronous operations in JavaScript/TypeScript applications, specifically by mocking external API calls and asserting their behavior using Jest. Mastering these techniques is crucial for writing reliable and maintainable tests for applications that rely on network requests.
Problem Description
You are tasked with creating a reusable Jest utility function that simplifies the process of mocking fetch calls and asserting the arguments they were called with. This utility should allow you to:
- Mock
fetch: Intercept allfetchcalls within a test. - Define Mock Responses: Specify the response for a given URL and HTTP method.
- Assert Call Arguments: Verify that
fetchwas called with the expected URL, method, and request body (if applicable).
Key Requirements
mockFetchfunction: This function should take an array of mock configurations as input. Each configuration should specify:url: The URL to match.method: The HTTP method (e.g., 'GET', 'POST').response: An object representing the mock response (includingstatusandjsonortext).requestBody(optional): The expected request body for assertion.
- Automatic Mocking: When
mockFetchis called, it should automatically replace the globalfetchwith a Jest mock function. - Response Resolution: When a
fetchcall is made, the utility should find a matching configuration based on URL and method. If a match is found, it should return aPromisethat resolves with the specified mock response. If no match is found, it should throw an error indicating an unmockedfetchcall. - Assertion Capabilities: The mocked
fetchshould store information about each call, allowing for assertions likeexpect(fetch).toHaveBeenCalledWith(...). - Cleanup: The utility should provide a way to reset the mocks between tests to avoid interference.
Expected Behavior
- Tests using this utility should be able to define expected API calls and their responses, and then assert that these calls were made correctly.
- Unmatched
fetchcalls should cause tests to fail, highlighting missing mock configurations.
Edge Cases to Consider
- Multiple
fetchcalls: The utility should handle multiplefetchcalls within a single test. - Different HTTP methods: Support for various methods like 'GET', 'POST', 'PUT', 'DELETE', etc.
- Request bodies: Correctly matching and asserting JSON or string request bodies.
- No request body: Handling
fetchcalls without abodyproperty. - Error handling: How to mock responses for non-2xx status codes.
Examples
Example 1: Mocking a GET request
// mockFetchUtility.ts
import { MockFetchConfig } from './types'; // Assume types are defined elsewhere
export function mockFetch(configs: MockFetchConfig[]): void {
// Implementation...
}
export function resetMockFetch(): void {
// Implementation to reset mocks...
}
// your-app/api.ts
async function getUser(userId: string): Promise<any> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
// your-app/api.test.ts
import { mockFetch, resetMockFetch } from './mockFetchUtility';
import { getUser } from './api';
describe('getUser', () => {
beforeEach(() => {
resetMockFetch(); // Ensure mocks are clean before each test
});
it('should fetch user data correctly', async () => {
const mockUserData = { id: '123', name: 'John Doe' };
mockFetch([
{
url: '/api/users/123',
method: 'GET',
response: { status: 200, json: () => Promise.resolve(mockUserData) },
},
]);
const userData = await getUser('123');
expect(userData).toEqual(mockUserData);
// Assert that fetch was called with the correct arguments
expect(fetch).toHaveBeenCalledWith('/api/users/123', { method: 'GET' });
});
it('should throw an error if user is not found', async () => {
mockFetch([
{
url: '/api/users/456',
method: 'GET',
response: { status: 404, json: () => Promise.resolve({ message: 'User not found' }) },
},
]);
await expect(getUser('456')).rejects.toThrow('HTTP error! status: 404');
expect(fetch).toHaveBeenCalledWith('/api/users/456', { method: 'GET' });
});
});
Example 2: Mocking a POST request with a body
// your-app/resource.ts
async function createResource(data: any): Promise<any> {
const response = await fetch('/api/resources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
// your-app/resource.test.ts
import { mockFetch, resetMockFetch } from './mockFetchUtility';
import { createResource } from './resource';
describe('createResource', () => {
beforeEach(() => {
resetMockFetch();
});
it('should create a resource with the correct data', async () => {
const resourceData = { name: 'New Item', value: 100 };
const mockResponse = { id: 'res-789', ...resourceData };
mockFetch([
{
url: '/api/resources',
method: 'POST',
response: { status: 201, json: () => Promise.resolve(mockResponse) },
requestBody: JSON.stringify(resourceData), // Expecting the stringified body
},
]);
const createdResource = await createResource(resourceData);
expect(createdResource).toEqual(mockResponse);
expect(fetch).toHaveBeenCalledWith('/api/resources', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(resourceData),
});
});
});
Constraints
- The solution must be implemented in TypeScript.
- Jest must be used as the testing framework.
- The
fetchAPI should be mocked. - The
mockFetchfunction should accept an array of configurations. - Each mock configuration must include
url,method, andresponse. requestBodyshould be an optional parameter for matching.- The mocked
fetchmust be accessible via Jest'sexpect(fetch).toHaveBeenCalledWith(...). - Performance is not a primary concern for this utility, but it should not introduce significant overhead to tests.
Notes
- Consider using Jest's
jest.fn()to create mock functions. - You'll need to manage the global
fetchby saving and restoring it. - Think about how to structure your mock response objects to mimic the real
Responseobject fromfetch. Specifically, how to handleresponse.json()andresponse.text(). - The
requestBodymatching should be strict. For JSON, you might need to stringify the inputrequestBodybefore comparison if the realfetchcall also stringifies it. - Your utility should aim to be generic enough to handle common
fetchscenarios. - Consider creating a type definition for
MockFetchConfigto ensure type safety.