The Problem With Mocking fetch
// Traditional approach — fragile
jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ id: '1', name: 'Alice' }),
} as Response);
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
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 }
);
}),
];
Browser Setup (for Storybook or dev)
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// 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 />);
});
Node.js Setup (for Jest/Vitest)
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/setupTests.ts
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
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();
});
});
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',
});
});
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)