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

  1. Monkey-patching fetch: Overwriting window.fetch = jest.fn().
    • Problem: You have to reconstruct the entire Response object manually.
  2. 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.
  3. 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. The Solution: Mock Service Worker (MSW)

Think of MSW as a man-in-the-middle proxy that lives directly inside your testing environment. It is the industry standard because it is implementation agnostic.

When your React component calls fetch('/api/user'), MSW intercepts that request before it leaves the browser (or Node environment) and returns a realistic mocked response.

  • Browser (Development): Uses a Service Worker. You can actually see the mocked requests successfully resolving in the “Network” tab of Chrome DevTools!
  • Node (Jest/Vitest/Playwright): Uses node-request-interceptor to patch the underlying http/https modules, so your component code remains completely unaware it’s being tested.

Your component simply says, “Give me user data,” and MSW acts as the mock backend delivering it. This eliminates flaky tests caused by real API downtime and allows you to test edge cases (like 500 errors) reliably.

Interactive: MSW Control Panel

Simulate different backend conditions to see how the frontend application reacts. In a real testing environment, MSW allows you to programmatically trigger these states.

MSW Configuration

500ms

App View

Click "Fetch User Data" to start.
> Network Log: Ready

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();
});
Architectural Warning: Mock Network, Not Logic

Do not put complex business logic in your MSW handlers. Mocks should return simple static responses or use simple conditionals. If you find yourself recreating database state or complex filtering logic inside your mock handlers, you are building a fake backend and testing your mocks, not your frontend application. Keep handlers thin and deterministic.