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:
- Mock the
fetchAPI: Intercept any calls tofetchmade by theDataService. - Simulate API responses: Provide different mock responses for successful data retrieval and error conditions.
- Isolate
DataServicetests: Ensure that tests forDataServicedo not interfere with each other or with tests of other modules. - Test error handling: Verify that the
DataServicecorrectly handles API errors.
Key Requirements:
- Use Jest's
jest.mockandjest.fn()to mock the globalfetchfunction. - Implement a test setup that allows you to configure the mock
fetchto return specific responses for different test cases. - Write tests that demonstrate the
DataServicecorrectly fetching data and handling errors. - Ensure that mocks are reset between tests to maintain isolation.
Expected Behavior:
- When
fetchUserDatais called, and the mockfetchis configured for success, it should return the expected user data. - When
fetchUserDatais called, and the mockfetchis configured to simulate an API error (e.g., a 404 or 500 status code), theDataServiceshould throw an appropriate error. - When
saveSettingsis called, and the mockfetchis 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
DataServiceclass 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 toglobal.fetch = jest.fn(). - Think about how you can manage multiple
fetchcalls within a single test if your methods become more complex.mockImplementationormockImplementationOncemight 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
beforeEachhook for mock cleanup and isolation.