Skip to main content
Back to Practice API
Mock Server Lab

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.

Playwright
await page.route('**/api/v1/users', (route) =>
  route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ data: [], total: 0 }),
  }),
);
MSW
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/v1/users', () =>
    HttpResponse.json({ data: [], total: 0 }),
  ),
];
nock
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}

Playwright
await page.route('**/api/v1/users', async (route) => {
  await new Promise((r) => setTimeout(r, 2000));
  await route.continue();
});
MSW
import { http, HttpResponse, delay } from 'msw';

http.get('/api/v1/users', async () => {
  await delay(2000);
  return HttpResponse.json({ data: [] });
});
nock
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)

Playwright
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();
});
MSW
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
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.

Playwright
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)) }),
);
MSW
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 });
});
nock
['processing', 'processing', 'completed'].forEach((status) => {
  nock('https://api.qa-practice.dev').get(/\/api\/v1\/jobs\/\d+/).reply(200, { status });
});

Related Labs