Mocking Modules

Mocking Modules and the Network

Two things in a component test have to be replaced: modules that cannot run under jsdom 21,691 — a maps SDK, anything touching canvas or WebGL — and the network. vi.mock(path, factory) swaps a module for whatever the factory returns, hoisted above the imports so the mock is in place before the component loads, which is why the mocked module can also be imported normally in the same file.

src/Where.test.jsx: standing in for a geocoding moduleJavaScript
import { reverseGeocode } from './geo';          // resolves to the mock below
vi.mock('./geo', () => ({ reverseGeocode: vi.fn(async () => 'Kuala Lumpur') }));
test('shows the city the geocoder returned', async () => {
  render(<Where lat={3.14} lon={101.7} />);
  expect(screen.getByText('Locating...')).toBeInTheDocument();
  expect(await screen.findByText('Kuala Lumpur')).toBeInTheDocument();
  expect(reverseGeocode).toHaveBeenCalledWith(3.14, 101.7);
});

The imported reverseGeocode is the mock, so one binding serves as both the stub and the spy. A second test calls reverseGeocode.mockResolvedValueOnce('Penang'), overriding a single call without touching the module-level default; afterEach(() => vi.clearAllMocks()) resets the recorded calls between tests, while vi.resetAllMocks() also discards the implementations. Mocking a component works the same way — return a stub rendering only the props you care about.

Do not mock fetch. A stubbed fetch asserts that your component called a function, not that it sent a correct request, and it keeps passing after you migrate to Axios 109,234 or TanStack Query 79,069 . Mock Service Worker 668,066 (mswjs/msw (https://github.com/mswjs/msw 18,227 )) 2.15.0, installed with npm 2,036 i -D msw, intercepts at the transport layer instead, so your code performs a genuine request and MSW answers it.

src/Greeting.test.jsx: a server that lives in the test fileJavaScript
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const url = 'https://api.example.com/greeting';
const server = setupServer(http.get(url, () => HttpResponse.json({ greeting: 'Hi there' })));
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('reports a server error', async () => {
  server.use(http.get(url, () => new HttpResponse(null, { status: 500 })));
  render(<Greeting url={url} />);
  await userEvent.setup().click(screen.getByRole('button'));
  expect(await screen.findByRole('alert')).toHaveTextContent(/could not load/i);
  expect(screen.getByRole('button')).toBeEnabled();
});

That test and a happy-path sibling pass in 243 ms. Three lines carry most of the value. onUnhandledRequest: 'error' fails the run when a component calls an endpoint nobody declared; server.use(...) adds a handler for one test only and resetHandlers() removes it. Because handlers are ordinary functions, the same file can drive the dev server through setupWorker.