sinon stubs and fake timers: working versions (tested 2026-10-18)
Stub object methods with resolves/rejects and onCall sequences, record spy call counts and args, and drive debounce and retry logic with sinon fake timers and clock.tickAsync under node:test, fully offline.
passed1 passed · 0 failed2026-10-18 03:58 UTC- Status
- passed
- Last passed
- 2026-10-18 03:58 UTC
- Tests
- 1 / 1
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
sinon@22.1.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 39095442401
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sinon-stubs-fake-timers") (install), or see them on GitHub.
package.json
{
"name": "sinon-stubs-fake-timers",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"devDependencies": {
"sinon": "22.1.0",
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/service.ts
export interface ApiClient {
fetchJob(id: string): Promise<{ id: string; status: string }>;
}
export async function loadStatus(client: ApiClient, id: string): Promise<string> {
const job = await client.fetchJob(id);
return job.status;
}
export function debounce<A extends unknown[]>(
fn: (...args: A) => void,
waitMs: number,
): (...args: A) => void {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: A) => {
if (timer !== undefined) {
clearTimeout(timer);
}
timer = setTimeout(() => {
timer = undefined;
fn(...args);
}, waitMs);
};
}
export async function retry<T>(
fn: () => Promise<T>,
opts: { attempts: number; delayMs: number },
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= opts.attempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < opts.attempts) {
await new Promise((resolve) => setTimeout(resolve, opts.delayMs * attempt));
}
}
}
throw lastError;
}
test/service.test.ts
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
import sinon from "sinon";
import { debounce, loadStatus, retry } from "../src/service.ts";
afterEach(() => {
sinon.restore();
});
test("stub resolves and rejects an object method", async () => {
const client = { fetchJob: sinon.stub() };
client.fetchJob.resolves({ id: "j1", status: "done" });
assert.equal(await loadStatus(client, "j1"), "done");
assert.deepEqual(client.fetchJob.firstCall.args, ["j1"]);
client.fetchJob.rejects(new Error("boom"));
await assert.rejects(() => loadStatus(client, "j2"), /boom/);
});
test("stub onCall returns a scripted sequence", async () => {
const fetchJob = sinon.stub();
fetchJob.onFirstCall().resolves({ id: "1", status: "queued" });
fetchJob.onSecondCall().rejects(new Error("nope"));
fetchJob.onThirdCall().resolves({ id: "1", status: "done" });
const client = { fetchJob };
assert.equal(await loadStatus(client, "1"), "queued");
await assert.rejects(() => loadStatus(client, "1"), /nope/);
assert.equal(await loadStatus(client, "1"), "done");
assert.equal(fetchJob.callCount, 3);
});
test("spy records call count and arguments", () => {
const logger = { log: (_msg: string): void => undefined };
const spy = sinon.spy(logger, "log");
logger.log("hello");
logger.log("world");
assert.equal(spy.callCount, 2);
assert.deepEqual(spy.firstCall.args, ["hello"]);
assert.deepEqual(spy.secondCall.args, ["world"]);
assert.ok(spy.calledWith("hello"));
});
test("debounce collapses calls using fake timers", async () => {
const clock = sinon.useFakeTimers();
const handler = sinon.spy();
const debounced = debounce(handler, 100);
debounced("a");
clock.tick(50);
debounced("b");
assert.equal(handler.callCount, 0);
await clock.tickAsync(100);
assert.equal(handler.callCount, 1);
assert.deepEqual(handler.firstCall.args, ["b"]);
clock.restore();
});
test("retry backs off between attempts using tickAsync", async () => {
const clock = sinon.useFakeTimers();
const fn = sinon.stub();
fn.onFirstCall().rejects(new Error("temporary"));
fn.onSecondCall().resolves("ok");
const pending = retry(fn, { attempts: 3, delayMs: 100 });
await clock.tickAsync(100);
assert.equal(await pending, "ok");
assert.equal(fn.callCount, 2);
clock.restore();
});
test("sandbox restore returns original behavior", async () => {
const sandbox = sinon.createSandbox();
const repo = { find: async (): Promise<string> => "real" };
sandbox.stub(repo, "find").resolves("stubbed");
assert.equal(await repo.find(), "stubbed");
sandbox.restore();
assert.equal(await repo.find(), "real");
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.