Hone logo
Hone
Problems

Jest Sandbox Environment for Isolated Testing

Testing complex or stateful JavaScript/TypeScript modules can be challenging due to their interconnectedness. This challenge asks you to create a robust testing strategy using Jest's sandboxing capabilities to isolate dependencies and ensure predictable test outcomes. You'll simulate a controlled environment for modules that might interact with external services, global state, or have side effects, making your tests more reliable and maintainable.

Problem Description

Your task is to create a Jest test suite for a hypothetical DataService class that interacts with an external API. The DataService has methods like fetchUserData and saveSettings that, in a real-world scenario, would involve network requests or database operations.

To make these tests fast, reliable, and avoid actual external calls, you need to implement a sandbox environment using Jest's mocking features. Specifically, you should:

  1. Mock the fetch API: Intercept any calls to fetch made by the DataService.
  2. Simulate API responses: Provide different mock responses for successful data retrieval and error conditions.
  3. Isolate DataService tests: Ensure that tests for DataService do not interfere with each other or with tests of other modules.
  4. Test error handling: Verify that the DataService correctly handles API errors.

Key Requirements:

  • Use Jest's jest.mock and jest.fn() to mock the global fetch function.
  • Implement a test setup that allows you to configure the mock fetch to return specific responses for different test cases.
  • Write tests that demonstrate the DataService correctly fetching data and handling errors.
  • Ensure that mocks are reset between tests to maintain isolation.

Expected Behavior:

  • When fetchUserData is called, and the mock fetch is configured for success, it should return the expected user data.
  • When fetchUserData is called, and the mock fetch is configured to simulate an API error (e.g., a 404 or 500 status code), the DataService should throw an appropriate error.
  • When saveSettings is called, and the mock fetch is configured for success, it should correctly send the settings data to the mocked API endpoint.

Examples

Example 1: Successful User Data Fetch

Input (Conceptual): Imagine a DataService class defined in dataService.ts:

// dataService.ts
export class DataService {
  async fetchUserData(userId: string): Promise<any> {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`Failed to fetch user data: ${response.statusText}`);
    }
    return response.json();
  }
}

And a test file dataService.test.ts:

// dataService.test.ts
import { DataService } from './dataService';

// Mock fetch globally
global.fetch = jest.fn();

describe('DataService', () => {
  let dataService: DataService;

  beforeEach(() => {
    dataService = new DataService();
    // Reset mocks before each test
    (global.fetch as jest.Mock).mockClear();
  });

  it('should fetch user data successfully', async () => {
    const mockUserData = { id: '123', name: 'John Doe' };

    // Configure mock fetch for this specific test
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUserData,
      statusText: 'OK',
    });

    const userData = await dataService.fetchUserData('123');

    expect(userData).toEqual(mockUserData);
    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(global.fetch).toHaveBeenCalledWith('/api/users/123');
  });

  // ... other tests
});

Output: The test passes. userData will be { id: '123', name: 'John Doe' }, and fetch will have been called exactly once with the correct URL.

Explanation: The fetch function is mocked using jest.fn(). mockResolvedValueOnce is used to define the return value for the first (and in this case, only) call to fetch within this test. The test then asserts that the fetchUserData method returns the expected data and that fetch was called correctly.

Example 2: API Error Handling

Input (Conceptual): Using the same dataService.ts as Example 1.

// dataService.test.ts (continued)
describe('DataService', () => {
  // ... beforeEach and previous test

  it('should throw an error if fetching user data fails', async () => {
    // Configure mock fetch to simulate an error
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: false,
      statusText: 'Not Found',
    });

    await expect(dataService.fetchUserData('456')).rejects.toThrow('Failed to fetch user data: Not Found');
    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(global.fetch).toHaveBeenCalledWith('/api/users/456');
  });
});

Output: The test passes. An error with the message "Failed to fetch user data: Not Found" is thrown and caught by expect(...).rejects.toThrow().

Explanation: Here, mockResolvedValueOnce is configured to simulate an unsuccessful API response (ok: false). The expect(...).rejects.toThrow() assertion is used to verify that the fetchUserData method correctly throws an error when the API call fails.

Example 3: Mocking a different API endpoint (if DataService had another method)

Input (Conceptual): Imagine DataService also has saveSettings:

// dataService.ts (extended)
export class DataService {
  // ... fetchUserData

  async saveSettings(settings: Record<string, any>): Promise<void> {
    const response = await fetch('/api/settings', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(settings),
    });
    if (!response.ok) {
      throw new Error(`Failed to save settings: ${response.statusText}`);
    }
  }
}

And a test file dataService.test.ts (with additional test):

// dataService.test.ts (continued)
describe('DataService', () => {
  // ... beforeEach and previous tests

  it('should save settings successfully', async () => {
    const mockSettings = { theme: 'dark', notifications: true };

    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      statusText: 'OK',
    });

    await dataService.saveSettings(mockSettings);

    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(global.fetch).toHaveBeenCalledWith('/api/settings', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(mockSettings),
    });
  });
});

Output: The test passes. fetch is called with the correct URL, method, headers, and JSON stringified body.

Explanation: This demonstrates how to mock fetch for a different API endpoint and HTTP method (POST) within the same DataService class. The mock is configured to represent a successful POST operation.

Constraints

  • The DataService class will be provided for you.
  • You must use TypeScript for your solution.
  • Your test suite should reside in a file named dataService.test.ts.
  • Ensure that all mocked functions are properly reset between individual test cases.
  • Avoid making any actual network requests during testing.

Notes

  • Consider using jest.spyOn(global, 'fetch') as an alternative or complement to global.fetch = jest.fn().
  • Think about how you can manage multiple fetch calls within a single test if your methods become more complex. mockImplementation or mockImplementationOnce might be useful.
  • The goal is to create a flexible mocking setup that can be easily adapted for different API responses and scenarios.
  • Pay close attention to the beforeEach hook for mock cleanup and isolation.
Loading editor...
typescript