Hone logo
Hone
Problems

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:

  1. Mock fetch: Intercept all fetch calls within a test.
  2. Define Mock Responses: Specify the response for a given URL and HTTP method.
  3. Assert Call Arguments: Verify that fetch was called with the expected URL, method, and request body (if applicable).

Key Requirements

  • mockFetch function: 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 (including status and json or text).
    • requestBody (optional): The expected request body for assertion.
  • Automatic Mocking: When mockFetch is called, it should automatically replace the global fetch with a Jest mock function.
  • Response Resolution: When a fetch call is made, the utility should find a matching configuration based on URL and method. If a match is found, it should return a Promise that resolves with the specified mock response. If no match is found, it should throw an error indicating an unmocked fetch call.
  • Assertion Capabilities: The mocked fetch should store information about each call, allowing for assertions like expect(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 fetch calls should cause tests to fail, highlighting missing mock configurations.

Edge Cases to Consider

  • Multiple fetch calls: The utility should handle multiple fetch calls 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 fetch calls without a body property.
  • 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 fetch API should be mocked.
  • The mockFetch function should accept an array of configurations.
  • Each mock configuration must include url, method, and response.
  • requestBody should be an optional parameter for matching.
  • The mocked fetch must be accessible via Jest's expect(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 fetch by saving and restoring it.
  • Think about how to structure your mock response objects to mimic the real Response object from fetch. Specifically, how to handle response.json() and response.text().
  • The requestBody matching should be strict. For JSON, you might need to stringify the input requestBody before comparison if the real fetch call also stringifies it.
  • Your utility should aim to be generic enough to handle common fetch scenarios.
  • Consider creating a type definition for MockFetchConfig to ensure type safety.
Loading editor...
typescript