mrkeyoor.com_
Sun 20 Sept 17:50 UTC
npmTestingupdated 20 Sept 2026

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`.

Verdict

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

Lab card: what happened when we installed sinonScreenshot of sinon documentation
Install✓ · 1.5s9 packages on disk · 5 MB
ImportESM import works · require() works · ESM package with exports map
Browser49.6 KBgzipped (167.8 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 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.

API stability4/5Spy, stub, sandbox, matcher, fake-timer, and assertion concepts remain familiar, and 22.1 consists of focused fixes rather than another surface rewrite. Its changes still affect subtle tests: sandbox call-order IDs are now isolated, `returns()` can override `returnsArg()`, `throwArg()` validates the boundary correctly, and `restoreObject()` is idempotent. Major releases have also changed runtime support and default timer behavior, so upgrade notes cannot be skipped.
Docs5/5sinonjs.org documents spies, stubs, mocks, sandboxes, matchers, assertions, fake timers, replacement helpers, and compatibility, with examples for behavior by argument and call number. The repository's change log explains 22.1 fixes down to the previous failure mechanism. The difficult topic is module seams: readers must distinguish object-property replacement, CommonJS link seams, immutable ESM namespaces, and test-runner module mocking before choosing an approach.
Maintenance5/5npm published 22.1.0 on 2026-07-20, and GitHub recorded a push on 2026-08-05. The unarchived repository has 9,758 stars and 59 open issues and pull requests. Recent work fixed parallel sandbox ordering, repeat cleanup, invalid throwArg boundaries, behavior precedence, Temporal-aware timers, and a sub-sandbox restore regression. The project also uses artifact contract tests to protect published entry points while source migrates.
Ecosystem5/5npm counted 11,857,903 downloads for the week ending 2026-08-23. Sinon remains runner-neutral and its related packages for fake timers, deep matching, and shared utilities are used beyond Sinon itself. Community adapters connect it to assertion styles and older module-seam tools, while DefinitelyTyped supplies its declarations. Current Jest and Vitest suites reduce the need for it because those runners bundle overlapping mock APIs.

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.
Skip it if

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

PackageRegistryPick it when
vitestnpmUse it when choosing a current test runner and you want spies, module mocks, timers, coverage, and execution in one tool.
testdoublenpmUse it for a smaller, opinionated test-double vocabulary and you do not need Sinon's full behavior matrix.
jest-mocknpmUse 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.