Lesson 12 / 25

Mocking the Network with MSW

Intercept requests instead of replacing fetch.

Mock at the network boundary

Mocking fetch or axios directly ties tests to one HTTP client and to exact call shapes. Mock Service Worker (MSW) intercepts requests at the network level, so your real fetch code, headers, URL building and JSON parsing all run. You describe request handlers (http.get('/api/user/:id', resolver)) that return HttpResponse.json(...). In Node test runners, setupServer from msw/node installs them; in the browser, the same handlers can run in a service worker for development. A common pattern: default happy-path handlers in a shared file, server.use(...) inside a test to override one endpoint with an error or empty state, and server.resetHandlers() after each test. The examples use the MSW 2.x API; earlier versions used rest and res(ctx...), so check the docs for your version.

MSW server in a setup file and a per-test override

MSW 2.x syntax.

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

export const handlers = [
  http.get('/api/users/:id', ({ params }) =>
    HttpResponse.json({ id: params.id, name: 'Asha' }),
  ),
];

// src/test/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

// src/test/setup.ts (listed in setupFiles)
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from './server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// users.test.ts
import { http, HttpResponse } from 'msw';
import { server } from './test/server';
import { fetchUser } from './users';

it('surfaces a server error', async () => {
  server.use(http.get('/api/users/:id', () => new HttpResponse(null, { status: 500 })));
  await expect(fetchUser('42')).rejects.toThrow(/500/);
});

Fail on unhandled requests

onUnhandledRequest: 'error' makes any request without a handler fail the test, so a typo in a URL or a forgotten endpoint is caught immediately instead of silently hitting nothing. Relative URLs in Node may need a base URL in your client config.

Quick check: What is the main advantage of MSW over mocking fetch directly?

  • It makes real calls to production
  • Your real request code runs and tests do not depend on which HTTP client you use
  • It removes the need for async tests
  • It only works with GraphQL
Answer

Your real request code runs and tests do not depend on which HTTP client you use — Mock the network, not the client.