-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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();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 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'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}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.
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook