Mocking fetch in Tests
Stub fetch without lying — MSW vs vi.fn, Response shapes, error paths, and cleanup so network tests stay deterministic.
- testing
- mocking-fetch
Frontend features live on HTTP. Tests must control responses without hitting real servers. You can mock global.fetch directly or intercept at the network layer with MSW. Prefer MSW for integration; direct mocks for tiny unit tests of a single client function.
Docs: MSW, Vitest mock functions, Fetch Response.
Direct fetch mock (unit)
import { vi, expect, test, afterEach } from 'vitest';
import { loadUser } from './loadUser';
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
test('loadUser parses JSON', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: '1', name: 'Ada' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
),
);
await expect(loadUser('1')).resolves.toEqual({ id: '1', name: 'Ada' });
expect(fetch).toHaveBeenCalledWith('/api/users/1', expect.anything());
});
Use real Response objects — half-mocked { json: async () => … } objects miss ok, status, headers.
Error paths
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(new Response(null, { status: 404 })),
);
await expect(loadUser('missing')).rejects.toThrow(/404/);
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValue(new TypeError('Failed to fetch')),
);
Why MSW scales better
- Works with any client (
fetch, axios, GraphQL). - Handlers readable as HTTP contracts.
- Same handlers in browser (Storybook/dev) and Node tests.
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users/:id', ({ params }) =>
HttpResponse.json({ id: params.id, name: 'Ada' }),
),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Asserting request bodies
http.post('/api/login', async ({ request }) => {
const body = await request.json();
expect(body).toEqual({ email: 'a@b.com', password: 'secret' });
return HttpResponse.json({ token: 't' });
});
Or spy on fetch mock calls — MSW keeps tests closer to wire format.
Cleanup rules
- Restore fetch after each test if stubbing globals.
resetHandlersso one test’s override doesn’t leak.- Don’t leave
mockImplementationfrom a prior case.
Footguns
- Forgetting
awaitonres.json(). - Mocking only happy status 200.
- Absolute vs relative URL mismatches in handlers.
- Parallel tests sharing one mutable mock implementation.
Interview out-loud answer
“For a single client function I stub fetch with real Response objects. For feature tests I use MSW handlers so any HTTP client works. I always cover non-OK and network failure, and I reset mocks/handlers between tests.”
Absolute URLs
Apps that call https://api.example.com/v1/... need handlers for that host or a relative base URL in tests. MSW can match full URLs; fetch mocks must compare the same string the client uses. Centralize API_BASE so tests and app agree.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Notes from real codebases
Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.
Related on this site
- MSW mock service worker
- Typing fetch responses
- Integration testing UI
- Contract testing APIs
- Flaky tests common causes
Further reading
Related guides
- Accessibility Testing in CIWire axe and lint rules into CI without false confidence — what automation catches, what it misses, and a practical pipeline.
- Component Testing StorybookUse Storybook as a component workshop and optional test runner — stories as living specs, interaction tests, and a11y checks.
- Contract Testing APIsKeep frontend and backend agreements honest — schema contracts, Pact-style consumer tests, and OpenAPI-driven checks without brittle E2E.
- Coverage Metrics PitfallsWhy 100% line coverage can still ship bugs — gaming metrics, useless tests, and what coverage is actually good for.
- End-to-End Testing TradeoffsWhen E2E tests earn their keep, why they flake, and how to keep a small critical-path suite instead of a slow second frontend.