4 Patterns for Test Mocking
Deterministic test data without a real backend. Compare three popular tools โ Playwright route interception, MSW (Mock Service Worker), and nock โ on the same four real-world scenarios.
1. Static response override
Intent: Return a fixed payload regardless of the upstream API state.
When to use: Use when you need deterministic data for a test โ empty list, boundary values, a specific user.
await page.route('**/api/v1/users', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: [], total: 0 }),
}),
);import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/v1/users', () =>
HttpResponse.json({ data: [], total: 0 }),
),
];import nock from 'nock';
nock('https://api.qa-practice.dev')
.get('/api/v1/users')
.reply(200, { data: [], total: 0 });2. Delayed response (latency simulation)
Intent: Respond after N milliseconds to test loading states, timeouts, and optimistic UI.
When to use: Use for skeletons, request-in-flight indicators, or AbortController behaviour.
Try it against: /api/v1/delay/{ms}
await page.route('**/api/v1/users', async (route) => {
await new Promise((r) => setTimeout(r, 2000));
await route.continue();
});import { http, HttpResponse, delay } from 'msw';
http.get('/api/v1/users', async () => {
await delay(2000);
return HttpResponse.json({ data: [] });
});nock('https://api.qa-practice.dev')
.get('/api/v1/users')
.delay(2000)
.reply(200, { data: [] });3. Chaos / failure injection
Intent: Fail a percentage of requests or inject 5xx errors deliberately.
When to use: Use to verify retry logic, circuit-breakers, error boundaries, and fallback UI.
Try it against: /api/v1/chaos/* (built-in)
let attempt = 0;
await page.route('**/api/v1/users', (route) => {
attempt += 1;
if (attempt < 3) {
return route.fulfill({ status: 503, body: 'Service Unavailable' });
}
return route.continue();
});let attempt = 0;
http.get('/api/v1/users', () => {
attempt += 1;
if (attempt < 3) {
return new HttpResponse('Service Unavailable', { status: 503 });
}
return HttpResponse.json({ data: [] });
});nock('https://api.qa-practice.dev')
.get('/api/v1/users')
.times(2)
.reply(503)
.get('/api/v1/users')
.reply(200, { data: [] });4. Stateful sequence
Intent: Advance through a predefined sequence of responses per request.
When to use: Use for pagination, polling until a job completes, or optimistic update flows.
const responses = [
{ status: 'processing' },
{ status: 'processing' },
{ status: 'completed' },
];
let i = 0;
await page.route('**/api/v1/jobs/*', (route) =>
route.fulfill({ status: 200, body: JSON.stringify(responses[i++] ?? responses.at(-1)) }),
);const sequence = ['processing', 'processing', 'completed'];
let i = 0;
http.get('/api/v1/jobs/:id', () => {
const status = sequence[i++] ?? sequence.at(-1)!;
return HttpResponse.json({ status });
});['processing', 'processing', 'completed'].forEach((status) => {
nock('https://api.qa-practice.dev').get(/\/api\/v1\/jobs\/\d+/).reply(200, { status });
});