Skip to content

Cookbook: mocking

Eugene Lazutkin edited this page Jul 3, 2026 · 2 revisions

Cookbook: Mocking

io.mock(matcher, handler) intercepts matching requests and answers them from a handler instead of the network — no server, no fetch stubbing. It attaches itself on the first mock and composes with the rest of the stack (retry, cache, inspectors), so you test the real pipeline.

Mock an exact URL

A string matcher matches the request URL exactly. Exact matchers are canonicalized — the query is sorted and any fragment is dropped — so ?a=1&b=2 and ?b=2&a=1 hit the same mock. The handler returns any value, which is serialized as a 200 application/json response.

import io from 'double-meh';

io.mock('https://api.example.com/users/1', () => ({id: 1, name: 'Bob'}));

await io.get('https://api.example.com/users/1'); // -> {id: 1, name: 'Bob'}

io.mock.clear();

Match by prefix, RegExp, or function

A trailing * matches by prefix, a RegExp tests the URL, and a function receives the prepared request and context for arbitrary matching. The handler also receives (request, ctx).

io.mock('https://api.example.com/users/*', request => ({url: request.url}));
io.mock(/\/orders\/\d+$/, () => ({status: 'shipped'}));
io.mock(
  request => request.method === 'POST',
  () => ({ok: true})
);

Exact matches win over pattern rules; pattern rules are tried in registration order. A single request can bypass mocking entirely with mock: false — it goes straight to the real transport even when a rule matches.

Return a real Response

Return a Response to control status, headers, and body verbatim — for non-JSON payloads or specific status codes.

io.mock(
  'https://api.example.com/report.csv',
  () => new Response('a,b\n1,2', {status: 200, headers: {'content-type': 'text/csv'}})
);

const env = await io.full.get('https://api.example.com/report.csv');
env.status; // 200
env.data; // 'a,b\n1,2'

Compose with retry: 503 then 200

Because the mock sits below the retry service, a handler that fails then succeeds exercises the retry path exactly as production would.

let n = 0;
io.mock(
  'https://api.example.com/flaky',
  () =>
    new Response(JSON.stringify({ok: true}), {
      status: ++n < 2 ? 503 : 200,
      headers: {'content-type': 'application/json'}
    })
);

const data = await io.get('https://api.example.com/flaky', null, {
  retry: {retries: 2, initDelay: 0}
});
// the 503 is retried; resolves to {ok: true}

A server-free test harness

Route every request through a single catch-all handler with a function matcher, and reset state between tests. This is exactly the serve() / reset() pattern the repo's own suite uses.

const json = (data, init = {}) =>
  new Response(data === undefined ? null : JSON.stringify(data), {
    status: init.status || 200,
    headers: {'content-type': 'application/json', ...(init.headers || {})}
  });

const serve = handler => io.mock(() => true, handler);
const reset = () => {
  io.mock.clear(); // remove all mocks and detach the service
  io.cache.clear(); // drop any cached entries
};

// in a test:
serve(request => {
  if (request.method === 'POST') return json({id: 7}, {status: 201});
  return json({hello: 'world'});
});

await io.get('https://example.com/x'); // -> {hello: 'world'}
reset();

Unmatched requests fall through to the real transport, so you can mock just one endpoint and let the rest hit the network.

See also

Clone this wiki locally