sinon review
Sinon 22.1.0 is a test-runner-independent toolkit for spies, stubs, mocks, fake timers, matchers, replacement properties, and call assertions. A spy records calls while preserving behavior; a stub replaces behavior; a mock declares expectations; a sandbox groups fakes for cleanup. It works with Mocha, `node:test`, browser test pages, and other runners that do not already impose a mocking API. Version 22.1 makes `restoreObject()` safe to call repeatedly, isolates call-order counters per sandbox for parallel tests, lets `returns()` override `returnsArg()`, and reports an out-of-range `throwArg()` as a TypeError instead of throwing `undefined`.
Sinon 22.1.0 remains useful when the runner does not provide enough test doubles or one mocking API must span several runners. Skip it in a new Vitest or Jest suite unless a specific Sinon behavior justifies the extra API, separate types, cleanup rules, and fake-timer semantics.
We installed it
| Install | ✓ · 1.5s | 9 packages on disk · 5 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 49.6 KB | gzipped (167.8 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does sinon install cleanly?
Yes. In a fresh container with an empty cache, npm install sinon finished in 2 seconds, leaving 9 packages and 5 MB on disk. npm audit reported no known vulnerabilities.
How much does sinon add to a browser bundle?
49.6 KB gzipped (167.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does sinon work with both ESM and CommonJS?
Yes. Both import 'sinon' and require('sinon') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does sinon include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
sinon or vitest: which should you use?
vitest: Use it when choosing a current test runner and you want spies, module mocks, timers, coverage, and execution in one tool. Sinon 22.1.0 remains useful when the runner does not provide enough test doubles or one mocking API must span several runners.
When should you not use sinon?
Vitest or Jest already supplies the needed mock functions and timer controls. Adding Sinon creates a second vocabulary and cleanup model without solving a missing capability.
Use it if
- Your test runner lacks spies, programmable stubs, fake timers, and call-order assertions, or you need one API shared across several runners.
- Stub behavior must vary by argument, call index, promise outcome, callback, or a replacement implementation.
- A suite needs explicit sandboxes so parallel tests can isolate fakes and restore every wrapped property after each case.
- Timer tests need control over Date, intervals, animation frames, microtasks, and the Temporal API exposed through the current fake-timers dependency.
- Vitest or Jest already supplies the needed mock functions and timer controls. Adding Sinon creates a second vocabulary and cleanup model without solving a missing capability.
- You need to replace native ESM module exports. Module namespace properties are not writable, so Sinon cannot stub them directly; use dependency injection or runner-level module mocking.
- TypeScript declarations must ship with the runtime package. Our inspection found none, so typed projects need `@types/sinon` and its separate release cadence.
- Frontend bundle weight matters. Our whole-package browser build measured 167.8 KB minified and 49.6 KB gzipped, which is test tooling weight that should never enter production output.
- You need whole-module graph substitution rather than methods on an object. Sinon works on reachable functions and properties, while loaders or runner mocks own import interception.
Setup reality
We installed Sinon 22.1.0 in a fresh unprivileged Node 22 Bookworm container. npm completed in 1.5 seconds, leaving 9 packages and 5 MB on disk. The package has 4 direct dependencies, 0 peer dependencies, and 2,604 KB unpacked. npm audit found 0 known vulnerabilities at every severity. Our inspection found no TypeScript declarations. The package uses the BSD-3-Clause license.
Sinon is an ESM package with an exports map; both CommonJS require() and ESM import worked in our checks. TypeScript users need @types/sinon. A browser build importing the full package measured 167.8 KB minified and 49.6 KB gzipped. Keep it in test-only dependencies and ensure production bundlers do not follow test helpers. The repository does not commit generated lib/ output, so installing a Git checkout without running its build differs from installing the published npm artifact.
Cleanup is the first configuration decision. Create one sandbox per test or call sinon.restore() in afterEach; a stub left on a shared object contaminates later cases. Version 22.1 isolates call IDs per sandbox, which makes immediate-before and immediate-after assertions usable under parallel execution. A root sandbox does not imply that every separately created sandbox shares cleanup, so restore the sandbox that created each fake.
Fake timers cover more queues than old Sinon recipes assume. Since Sinon 19, the default set includes process.nextTick and queueMicrotask; use tickAsync() when promises must settle, or specify toFake narrowly. Spying calls the real function and keeps its side effects. Stubbing a missing or non-configurable property fails instead of inventing a seam. Native ESM exports are read-only, so design injectable collaborators rather than trying to patch an import namespace.
Patterns
Record calls while keeping the implementation spy-on-method
const saveSpy = sinon.spy(repository, 'save');
try {
await service.create(record);
sinon.assert.calledOnce(saveSpy);
sinon.assert.calledWith(saveSpy, record);
} finally {
saveSpy.restore();
}A spy still executes repository.save. Use a stub when the real write, request, or side effect must not happen.
Replace one method with fixed data stub-return-value
const lookup = sinon.stub(repository, 'findById').returns({ id: 7, active: true });
try {
assert.equal(service.isActive(7), true);
} finally {
lookup.restore();
}The target property must exist and be replaceable. Stubbing a typo fails instead of adding a new method.
Resolve and reject asynchronous collaborators stub-promises
sinon.stub(api, 'load').resolves({ id: 1 });
sinon.stub(api, 'remove').rejects(new Error('offline'));
await assert.deepEqual(await api.load(), { id: 1 });
await assert.rejects(api.remove(), /offline/);Await rejected calls in the test. An ignored rejected promise can become an unhandled rejection after the assertion has passed.
Change behavior on successive calls vary-by-call
const request = sinon.stub();
request.onFirstCall().rejects(new Error('temporary'));
request.onSecondCall().resolves({ ok: true });
await assert.rejects(request());
assert.deepEqual(await request(), { ok: true });onCall uses a zero-based index. Define a default behavior if the production code might make more calls than the test lists.
Select behavior with argument matchers vary-by-argument
const price = sinon.stub().returns(null);
price.withArgs('basic').returns(10);
price.withArgs(sinon.match(/^pro-/)).returns(30);
assert.equal(price('basic'), 10);
assert.equal(price('unknown'), null);withArgs behavior wins for matching calls while the base stub handles everything else.
Clean all fakes after every test restore-sandbox
let sandbox;
beforeEach(() => { sandbox = sinon.createSandbox(); });
afterEach(() => sandbox.restore());
it('logs one warning', () => {
const warning = sandbox.stub(logger, 'warn');
run();
sinon.assert.calledOnce(warning);
});Restore the sandbox that created the fake. Separate sandboxes are useful for parallel isolation but need separate cleanup.
Verify ordering and exact arguments assert-call-order
sinon.assert.calledOnce(validate);
sinon.assert.calledWithExactly(validate, payload);
sinon.assert.callOrder(validate, persist, publish);
sinon.assert.notCalled(rollback);Version 22.1 gives each sandbox its own call ID counter, avoiding cross-test interference in immediate call-order checks.
Match only relevant object fields match-partial-argument
sinon.assert.calledWith(
send,
sinon.match({ to: 'ada@example.com' }),
sinon.match.has('requestId'),
);sinon.match with an object is partial. Use calledWithExactly and a full expected value when extra arguments or fields should fail.
Advance selected fake timers asynchronously control-time
const clock = sinon.useFakeTimers({
now: new Date('2026-01-01T00:00:00Z'),
toFake: ['Date', 'setTimeout', 'clearTimeout'],
});
try {
const result = scheduleRetry();
await clock.tickAsync(5_000);
await result;
} finally {
clock.restore();
}Narrow toFake when microtasks should stay native. tickAsync lets promise continuations run between timer callbacks.
Create stubs without running a constructor stub-class-instance
const repository = sinon.createStubInstance(UserRepository);
repository.findById.resolves({ id: 1 });
repository.save.resolves();
const service = new UserService(repository);Only prototype methods are stubbed. Fields normally assigned by the constructor do not appear automatically.
Replace a value and a getter replace-property
sinon.replace(config, 'apiUrl', 'http://127.0.0.1:3000');
sinon.replaceGetter(session, 'currentUser', () => ({ id: 1 }));
try {
run();
} finally {
sinon.restoreObject(config);
sinon.restoreObject(session);
}restoreObject is idempotent in 22.1, but falsy targets still throw. Non-configurable properties cannot be replaced.
Run a stateful replacement function call-custom-implementation
const charge = sinon.stub(payments, 'charge').callsFake(async (amount) => {
if (amount <= 0) throw new RangeError('amount');
return { id: `test-${amount}`, status: 'paid' };
});
const receipt = await payments.charge(500);callsFake never invokes the wrapped original. Use callThrough deliberately when one argument case should execute real behavior.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| vitest | npm | Use it when choosing a current test runner and you want spies, module mocks, timers, coverage, and execution in one tool. |
| testdouble | npm | Use it for a smaller, opinionated test-double vocabulary and you do not need Sinon's full behavior matrix. |
| jest-mock | npm | Use it when Jest-compatible mock functions are needed outside the full Jest runner. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

