mrkeyoor.com_
Thu 06 Aug 15:39 UTC
npmTestingupdated 06 Aug 2026

sinon

Sinon gives you test doubles that do not care which test runner you use: spies that record how a function was called, stubs that replace a method with programmable behavior, mocks that assert expectations up front, and fake timers that let you control Date, setTimeout, setInterval, and the microtask queue. It also ships matchers for loose argument checks and its own assertion helpers that print the calls that actually happened when something fails. It works in Node and in browsers, and it predates the mocking built into modern runners, which is both why it exists and why fewer new projects reach for it.

Verdict

Still the best standalone mocking library in JavaScript and the right answer for Mocha, node:test, and browser suites. If your runner already has vi.fn or jest.fn, installing Sinon on top of it buys you argument-level stub behavior at the cost of a second API your team has to learn.

API stability3/5Core spy and stub methods have looked the same for a decade, but a major lands roughly every six to twelve months, and the Sinon 19 fake-timers change altered async test behavior without any code change on your side.
Docs5/5sinonjs.org documents every method individually with runnable examples, and the June 2026 move to VitePress added a guides section with a migration page, an FAQ, and how-tos for ESM stubbing, CommonJS link seams, and TypeScript.
Maintenance5/5Pushed the day before this review, 22.1.0 out in July 2026, 42 open issues, and the docs examples are executed in CI so the published snippets are tested rather than assumed.
Ecosystem5/5The @sinonjs org maintains fake-timers, samsam, and commons as separate packages other tools reuse, and the surrounding layer includes sinon-chai, sinon-test, proxyquire, and @types/sinon.

Use it if

  • Your runner has no mocking of its own: Mocha, AVA, node:test, Karma, or a plain browser test page
  • You need stub behavior that varies by argument or call index: withArgs, onCall, resolves, rejects, yields, and callsFake all compose on one stub
  • You need deep control of time, including Date, timers, requestAnimationFrame, process.nextTick, and the Temporal API through fake-timers 15
  • You want failure messages that show the recorded calls, which sinon.assert prints and a bare boolean assertion does not
Skip it if

Setup reality

npm i -D sinon and there are no peer dependencies to satisfy. The package exports both builds, CommonJS at lib/sinon.js and ESM at pkg/sinon-esm.js, so a default import works either way; note that lib/ is generated and not committed, which matters only if you install from a git ref. The real setup cost is restore discipline. Every spy or stub you attach to a shared object stays attached until something restores it, so an afterEach calling sinon.restore(), or an explicit sinon.createSandbox() per file, is not optional; without it one test leaks into the next and the failure shows up somewhere unrelated. TypeScript users add @types/sinon separately. If you use fake timers, expect to convert tick() calls to tickAsync() or narrow the toFake list once you are on Sinon 19 or newer.

Patterns

Record calls without changing behaviorspy-on-method

import sinon from 'sinon'

const anonymous = sinon.spy()

const save = sinon.spy(repo, 'save')
await service.createUser({ name: 'ada' })

console.log(save.calledOnce, save.firstCall.args)
save.restore()

A spy still runs the original method, so side effects such as real database writes still happen. Use a stub when you want the call blocked.

Replace a method with a fixed return valuestub-method

const findUser = sinon.stub(db, 'findUser').returns({ id: 1, name: 'ada' })

// restore a single stub
findUser.restore()

Since Sinon 4, stubbing a property that does not exist throws instead of inventing one, so a renamed method fails the test loudly rather than quietly returning undefined.

Stub promise-returning functionsstub-async

sinon.stub(api, 'fetchUser').resolves({ id: 1 })
sinon.stub(api, 'deleteUser').rejects(new Error('gateway down'))

const stub = sinon.stub(api, 'listUsers')
stub.onFirstCall().rejects(new Error('flaky'))
stub.onSecondCall().resolves([])

rejects() creates the rejected promise as soon as the stub is called, so a test that never awaits it can log an unhandled rejection warning even while passing.

Different behavior per argument or per callbehavior-per-argument

const stub = sinon.stub()

stub.returns('default')
stub.withArgs(42).returns('answer')
stub.withArgs(sinon.match.string).throws(new TypeError('numbers only'))
stub.onCall(2).returns('third time')

stub(1) // 'default'
stub(42) // 'answer'

withArgs matches win over the default behavior, and onCall is zero indexed, so onCall(2) fires on the third invocation.

Restore everything between testssandbox-cleanup

const sandbox = sinon.createSandbox()

afterEach(() => {
  sandbox.restore()
})

it('logs a warning', () => {
  const warn = sandbox.stub(logger, 'warn')
  doRiskyThing()
  sinon.assert.calledOnce(warn)
})

Since Sinon 5 the default export is itself a sandbox, so sinon.stub() plus sinon.restore() in afterEach also works. Skip the cleanup and stubs leak into every later test in the process.

Assert with readable failure outputassertions

sinon.assert.calledOnce(save)
sinon.assert.calledWithExactly(save, { id: 1, name: 'ada' })
sinon.assert.callOrder(validate, save, publish)
sinon.assert.notCalled(rollback)

assert(save.calledWith(x)) only ever tells you it was false. sinon.assert prints every recorded call and the expected arguments, which is the difference between a five second and a twenty minute debug.

Match arguments looselyargument-matchers

sinon.assert.calledWith(save, sinon.match({ role: 'admin' }))
sinon.assert.calledWith(send, sinon.match.string, sinon.match.has('subject'))
sinon.assert.calledWith(track, sinon.match.number.and(sinon.match((n) => n > 0)))

sinon.match({...}) is a partial deep match, so extra keys on the real argument are fine. Use calledWithExactly plus a full object when you want strictness.

Control time in a testfake-timers

const clock = sinon.useFakeTimers({
  now: new Date('2026-01-01T00:00:00Z'),
  toFake: ['setTimeout', 'setInterval', 'Date'],
})

scheduleRetry()
await clock.tickAsync(30_000)

clock.restore()

From Sinon 19 onward every timer is faked by default, including process.nextTick and queueMicrotask. If awaited promises stop resolving, switch tick() to tickAsync() or narrow toFake as shown here.

Build a fully stubbed collaborator from a classstub-instance

class UserRepository {
  async findById(id) {}
  async save(user) {}
}

const repo = sinon.createStubInstance(UserRepository)
repo.findById.resolves({ id: 1 })

const service = new UserService(repo)

The constructor is never invoked and only prototype methods are stubbed, so any field the real constructor would assign is undefined on the double.

Swap a plain property or a getterreplace-property

sinon.replace(config, 'apiUrl', 'http://localhost:1234')
sinon.replaceGetter(session, 'currentUser', () => ({ id: 1, role: 'admin' }))
sinon.replaceSetter(store, 'value', () => {})

sinon.restore()

replace throws if the property is already replaced or is not configurable, which is deliberate: it stops two tests silently fighting over the same global.

Substitute your own implementation, or fall throughcalls-fake

const charge = sinon.stub(payments, 'charge')

charge.callsFake(async (amountCents) => ({ ok: true, amountCents }))
charge.withArgs(0).callThrough() // real method for this case only

callThrough runs the wrapped original, so it only makes sense on a stub created from an existing object method, not on a bare sinon.stub().

Unwrap every double on one objectrestore-object

sinon.spy(mailer, 'send')
sinon.stub(mailer, 'queue').resolves()

sinon.restoreObject(mailer)

Made idempotent in 22.1, so calling it twice is safe and a no-op on objects with nothing to restore. It lives in the Utilities section, which the docs warn is not covered by the public API guarantee.

Alternatives

PackageRegistryPick it when
vitestnpmYou are picking a test stack now and want the runner, spies, fake timers, and module mocking from one project.
testdoublenpmYou prefer a smaller opinionated API with no spy/stub/mock distinction and terser failure output.
jest-mocknpmYou want Jest mock functions inside a non-Jest runner without adopting the rest of Jest.