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. 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-interceptor to 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

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();
});

[!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.