Mocking APIs
Frontend tests that rely on real backend APIs are flaky, slow, and hard to maintain. If the backend is down, your frontend tests fail. This is a false negative.
To solve this, we use Mocking. However, not all mocking strategies are created equal.
1. The Evolution of Mocking
- Monkey-patching
fetch: Overwritingwindow.fetch = jest.fn().- Problem: You have to reconstruct the entire
Responseobject manually.
- Problem: You have to reconstruct the entire
- Mocking the API client:
jest.mock('./api.js').- Problem: You aren’t testing the code that calls the API. You’re testing a mock of a function you wrote.
- Network Level Mocking (MSW): Intercepting the request at the network layer.
- Solution: The application sends a real network request. MSW intercepts it and returns a realistic response.
2. Why Mock Service Worker (MSW)?
MSW is the industry standard because it is implementation agnostic.
- Browser: Uses a Service Worker to intercept requests. You can see the mocked requests in the “Network” tab of DevTools!
- Node (Jest/Vitest): Uses
node-request-interceptorto patch http/https modules.
Your component code doesn’t know it’s being mocked. It just calls fetch('/user') and gets a response.
Interactive: MSW Control Panel
Simulate different backend conditions and see how the frontend reacts.
MSW Configuration
App View
3. Setting up MSW: Best Practices
1. Organize Handlers
Don’t dump everything in one file. Group handlers by domain.
// src/mocks/handlers/auth.js
import { http, HttpResponse } from 'msw';
export const authHandlers = [
http.post('/api/login', async ({ request }) => {
const { username } = await request.json();
if (username === 'admin') {
return HttpResponse.json({ token: 'abc-123' });
}
return new HttpResponse(null, { status: 403 });
}),
];
// src/mocks/handlers/user.js
export const userHandlers = [
http.get('/api/user', () => {
return HttpResponse.json({ name: 'Jules' });
}),
];
// src/mocks/handlers.js
import { authHandlers } from './handlers/auth';
import { userHandlers } from './handlers/user';
export const handlers = [...authHandlers, ...userHandlers];
2. Strict Mocking vs. Passthrough
By default, MSW warns you if a request is unhandled. You can configure it to error (strict mode) or pass through to the real internet (useful for loading assets like images).
// src/setupTests.js
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
To allow specific requests to pass through:
http.get('https://fonts.googleapis.com/*', ({ request }) => {
return passthrough();
})
4. Simulating Chaos: Testing the Unhappy Path
The real power of MSW is testing scenarios that are hard to reproduce with a real backend.
1. Network Errors (500)
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
test('shows error message when server explodes', async () => {
// Override the default handler for this specific test
server.use(
http.get('/api/user', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserProfile />);
await expect(screen.findByText(/something went wrong/i)).toBeVisible();
});
2. Network Latency (Loading States)
Test if your loading spinner appears correctly.
import { delay } from 'msw';
test('shows loading spinner while fetching', async () => {
server.use(
http.get('/api/user', async () => {
await delay(2000); // Wait 2 seconds
return HttpResponse.json({ name: 'Jules' });
})
);
render(<UserProfile />);
expect(screen.getByTestId('spinner')).toBeVisible();
// Wait for it to disappear
await expect(screen.findByText('Jules')).toBeVisible();
});
[!IMPORTANT] Mock Network, Not Logic: Do not put business logic in your mocks. Mocks should be simple static responses or simple conditionals. If your mocks are complex, you are testing your mocks, not your app.