DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Mock Service Worker: Test Your UI Without Mocking fetch

The Problem With Mocking fetch

// Traditional approach — fragile
jest.spyOn(global, 'fetch').mockResolvedValue({
  ok: true,
  json: async () => ({ id: '1', name: 'Alice' }),
} as Response);
Enter fullscreen mode Exit fullscreen mode

This breaks when you change the API response shape. It doesn't test your actual request headers, body, or URL patterns. It ties your tests to implementation details.

MSW (Mock Service Worker) intercepts requests at the network level—your code calls fetch normally, MSW intercepts it.

Setup

npm install msw --save-dev
npx msw init public/ --save
Enter fullscreen mode Exit fullscreen mode

Define Handlers

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  // GET /api/users
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' },
    ]);
  }),

  // GET /api/users/:id
  http.get('/api/users/:id', ({ params }) => {
    const { id } = params;
    if (id === 'not-found') {
      return new HttpResponse(null, { status: 404 });
    }
    return HttpResponse.json({ id, name: 'Alice', email: 'alice@example.com' });
  }),

  // POST /api/users
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json(
      { id: 'new-id', ...body },
      { status: 201 }
    );
  }),
];
Enter fullscreen mode Exit fullscreen mode

Browser Setup (for Storybook or dev)

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
Enter fullscreen mode Exit fullscreen mode
// src/main.tsx
async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return;
  const { worker } = await import('./mocks/browser');
  return worker.start({ onUnhandledRequest: 'bypass' });
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
});
Enter fullscreen mode Exit fullscreen mode

Node.js Setup (for Jest/Vitest)

// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
Enter fullscreen mode Exit fullscreen mode
// src/setupTests.ts
import { server } from './mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Enter fullscreen mode Exit fullscreen mode

Writing Tests

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';
import { UserList } from './UserList';

test('displays list of users', async () => {
  render(<UserList />);

  // Waits for real async fetch to complete
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

test('shows error state when API fails', async () => {
  // Override handler for this test only
  server.use(
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 });
    })
  );

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText('Failed to load users')).toBeInTheDocument();
  });
});

test('creates a user on form submit', async () => {
  const user = userEvent.setup();

  render(<CreateUserForm />);

  await user.type(screen.getByLabelText('Name'), 'Charlie');
  await user.type(screen.getByLabelText('Email'), 'charlie@example.com');
  await user.click(screen.getByRole('button', { name: 'Create' }));

  await waitFor(() => {
    expect(screen.getByText('User created successfully')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Request Assertions

test('sends correct payload', async () => {
  let capturedBody: unknown;

  server.use(
    http.post('/api/users', async ({ request }) => {
      capturedBody = await request.json();
      return HttpResponse.json({ id: 'new', ...capturedBody }, { status: 201 });
    })
  );

  // ... interact with form

  expect(capturedBody).toEqual({
    name: 'Charlie',
    email: 'charlie@example.com',
  });
});
Enter fullscreen mode Exit fullscreen mode

Why MSW Beats the Alternatives

Approach Tests real URL? Tests headers? Refactor-safe?
Mock fetch No No No
Mock module No No No
MSW Yes Yes Yes
Real server Yes Yes Yes (but slow)

MSW gives you most of the confidence of a real server with the speed of unit tests.


Testing setup with MSW, React Testing Library, and Vitest pre-configured: Whoff Agents AI SaaS Starter Kit.

Top comments (0)