Sinon.js Cheatsheet - Spy, Stub, Mock & Fake Timers

This reference is for JavaScript developers writing unit tests who need to isolate the code under test from its surroundings. It covers spies for observing calls, stubs to replace behavior with return/throw/resolve constants, strict mocks with expectations, fake timers to fast-forward setTimeout, and sandboxes that auto-restore everything. Later sections show the sinon.assert helpers and practical patterns like sinon.replace and parameter matchers. After reading you should be able to spy on a dependency, stub an external call, control time in tests, and clean up all doubles in a teardown.

Languages·46 commands·Last updated 2026-07-21
sinonTestingmockspystub

Typical Use Case

For developers writing unit tests in Mocha, Jest or similar runners who need dependencies to behave in a controlled way. The choice is the whole story: spy records without changing behaviour, stub replaces an implementation so you can force a branch, and mock binds an expectation to a call so a contract can be verified. Fake timers fast-forward setTimeout instead of making the suite sleep. Close with createSandbox in beforeEach and sandbox.restore() in afterEach, since manual restoration is where suites leak state.

Spy 8

sinon.spy(obj, "method")
Spy on an object's method calls
sinon.spy(fn)
Wrap a function as a spy
spy.calledOnce
Whether called exactly once
spy.calledWith(arg1, arg2)
Whether called with specific args
spy.returned(value)
Whether returned a value
spy.callCount
Get the call count
spy.args[0]
Get the first call's args array
spy.restore()
Restore the original method

Stub 9

sinon.stub(obj, "method")
Replace a method with an empty function
stub.returns(value)
Specify a return value
stub.throws(Error)
Throw an exception
stub.callsFake(fn)
Replace with a custom function
stub.resolves(value)
Return a resolved Promise (async)
stub.rejects(Error)
Return a rejected Promise (async)
stub.callsArg(0)
Call the first arg as a callback
stub.onCall(0).returns(value)
Return different values per call
stub.restore()
Restore the original method

Mock 7

sinon.mock(obj)
Create a mock object
mock.expects("method")
Expect a method to be called
expects.once()
Expect it called once
expects.withArgs(arg)
Expect it called with args
expects.never()
Expect it never called
mock.verify()
Verify all expectations
mock.restore()
Restore and clear all expectations

Fake Timers 6

sinon.useFakeTimers()
Take over setTimeout/setInterval
clock.tick(1000)
Advance 1000ms
clock.runAll()
Run all queued timers
clock.restore()
Restore real time
sinon.useFakeTimers({ now: 1000 })
Set an initial time
sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] })
Fake only specified APIs

Assertions & Verification 6

sinon.assert.called(spy)
Assert a spy was called
sinon.assert.calledOnce(spy)
Assert called once
sinon.assert.calledWith(spy, arg)
Assert called with args
sinon.assert.notCalled(spy)
Assert not called
sinon.assert.callCount(spy, 3)
Assert call count
sinon.assert.calledWithExactly(spy, arg)
Assert exact argument match

Sandbox 5

sinon.createSandbox()
Create an isolated sandbox (recommended in beforeEach)
sandbox.spy(obj, "method")
Create a spy inside the sandbox
sandbox.stub(obj, "method")
Create a stub inside the sandbox
sandbox.useFakeTimers()
Use fake timers inside the sandbox
sandbox.restore()
Restore everything in the sandbox at once

Useful Patterns 5

sinon.replace(obj, "method", fn)
Replace a method (preferred over direct stub)
sinon.replaceGetter(obj, "prop", fn)
Replace an object getter
sinon.replaceSetter(obj, "prop", fn)
Replace an object setter
sinon.fake()
Create a no-op fake function
sinon.match({ id: sinon.match.number })
Argument matcher (type/shape match)

Parameter matrix

ParameterEffectExample
sinon.spy(obj, "method")Wrap an object method, preserving the original behavior and recording callssinon.spy(obj, "method")
sinon.spy(fn)Wrap a function as a spy, counting calls and argumentssinon.spy(fn)
spy.calledOnceCheck whether it was called exactly onceexpect(spy.calledOnce).toBe(true)
spy.calledWith(a, b)Check whether it was ever called with these argumentsspy.calledWith(1, "x")
spy.returned(value)Check whether it ever returned the given valuespy.returned(200)
spy.callCountRead the cumulative call countspy.callCount
spy.args[0]Get the argument array of the first callspy.args[0][0]
spy.restore()Restore the wrapped original methodspy.restore()
sinon.stub(obj, "method")Replace the method with a programmable empty functionsinon.stub(obj, "method")
stub.returns(value)Return a fixed value on every callstub.returns(42)
stub.throws(Error)Throw on call, to test the error branchstub.throws(new Error("boom"))
stub.callsFake(fn)Replace the original function with a custom implementationstub.callsFake(() => 1)
stub.resolves(value)Return a resolved Promisestub.resolves({ id: 1 })
stub.rejects(Error)Return a rejected Promisestub.rejects(new Error("fail"))
stub.callsArg(0)Call the first argument as a callback, simulating callback-style APIsstub.callsArg(0)
stub.onCall(0).returns(v)Return different values by call orderstub.onCall(0).returns("a")
stub.restore()Restore the replaced original methodstub.restore()
sinon.mock(obj)Create a mock objectconst mock = sinon.mock(obj)
mock.expects("method")Expect a method to be calledmock.expects("save")
expects.once() / never()Expect one call / expect no callmock.expects("save").never()
expects.withArgs(arg)Expect a call with the given argumentmock.expects("save").withArgs(1)
mock.verify()Verify all declared expectationsmock.verify()
sinon.useFakeTimers()Take over setTimeout / setIntervalsinon.useFakeTimers()
clock.tick(1000)Fast-forward the fake clock by 1000 millisecondsclock.tick(1000)
clock.runAll()Drain all queued timersclock.runAll()
clock.restore()Restore real timeclock.restore()
useFakeTimers({ now })Set the initial time of the fake clocksinon.useFakeTimers({ now: 1000 })
useFakeTimers({ toFake })Fake only the specified APIssinon.useFakeTimers({ toFake: ["setTimeout", "Date"] })
sinon.assert.called(spy)Assert the spy was calledsinon.assert.called(spy)
sinon.assert.calledOnce(spy)Assert it was called exactly oncesinon.assert.calledOnce(spy)
sinon.assert.calledWith(spy, a)Assert the call passed in that argumentsinon.assert.calledWith(spy, 1)
sinon.assert.calledWithExactly(spy, a)Assert arguments match strictlysinon.assert.calledWithExactly(spy, 1)
sinon.assert.notCalled(spy)Assert it was not calledsinon.assert.notCalled(spy)
sinon.assert.callCount(spy, 3)Assert the call count is 3sinon.assert.callCount(spy, 3)
sinon.createSandbox()Create an isolated sandbox to manage all doublesconst sandbox = sinon.createSandbox()
sandbox.spy / stub / useFakeTimersCreate spies, stubs, and fake timers inside the sandboxsandbox.stub(obj, "method")
sandbox.restore()Restore all doubles in the sandbox at oncesandbox.restore()
sinon.replace(obj, "m", fn)Replace a method, the recommended formsinon.replace(obj, "method", fn)
sinon.replaceGetter / replaceSetterReplace an object getter / settersinon.replaceGetter(obj, "prop", fn)
sinon.fake()Create a fake function that does not change behaviorconst fake = sinon.fake()
sinon.match({ })Argument matcher that matches by type or structuresinon.match({ id: sinon.match.number })

Common pitfalls

SymptomAfter one test passes, the same-named method in the next test mysteriously behaves differently

CauseThe stub / spy from the previous case was not restored, so the original method stays replaced permanently

FixSwitch to sinon.createSandbox() and call sandbox.restore() uniformly in afterEach

SymptomTime-related logic hangs after the test finishes, or every later case stalls waiting

CauseAfter useFakeTimers takes over, real time is never restored, so the test process waits forever for the fake timers to advance

FixCall clock.restore() in afterEach, or let the sandbox restore everything uniformly

Symptomclock.tick(1000) is written, but the assertion inside the callback never runs

CauseThe callback registers new timers inside itself, or the assertion sits before the tick

FixUse clock.runAll() to drain the whole queue, and make sure the assertion is on the code path after the callback is triggered

Symptomawait on a stubbed async method returns undefined

CauseBy default a stub returns undefined rather than a Promise, so awaiting it yields nothing

FixUse stub.resolves(value) or stub.rejects(Error) to construct explicit success and failure paths

Symptomspy.calledWith is false, even though the code clearly called it

CauseWhen the target is a reference-type object, comparison by reference never matches

FixUse matchers like sinon.match({ id: sinon.match.number }), or compare with spy.calledWithMatch

SymptomCallback-style methods cannot exercise the callback body

CauseAfter stubbing, the passed-in callback is not executed by default

FixUse stub.callsArg(0) to call the first argument as a callback, or trigger it manually with callsFake

SymptomThe same method needs different return values across calls, but it returns the same every time

Causestub.returns is fixed and unrelated to the call index

FixUse stub.onCall(0).returns(...).onCall(1).returns(...) to configure by order

Troubleshooting

  1. 1Confirm whether a method was actually called

    sinon.assert.called(spy)

    When the assertion fails the message shows the actual calls directly, locating the issue faster than printing spy.args manually.

  2. 2Check whether the call count matches expectations

    sinon.assert.callCount(spy, 3)

    When the count is off, print spy.callCount and compare it against the loops and event bindings in the code under test.

  3. 3Make timing logic not depend on real waits

    const clock = sinon.useFakeTimers();

    Once taken over, you control time; pair with clock.tick(1000) to advance 1000 milliseconds instantly.

  4. 4Drain all queued timers

    clock.runAll()

    tick advances only the given duration, so chained timers may not have had their turn yet; use runAll to flush them all at once.

  5. 5Construct a successful return for an async method

    stub.resolves({ id: 1 });

    Simpler than wrapping an async function in callsFake, and what you await is exactly that value.

  6. 6Hand all doubles to the sandbox for management

    sandbox.restore();

    Calling it once in afterEach restores every spy, stub, and fake timer, eliminating cross-case contamination.

  7. 7Match complex arguments by type or structure

    sinon.match({ id: sinon.match.number })

    Object arguments fail to match by reference; use a matcher to validate only the fields you care about.

Command Examples

Manage every double in a sandbox and restore in one call

let sandbox beforeEach(() => { sandbox = sinon.createSandbox() }) afterEach(() => { sandbox.restore() })

A stub or spy that is never restored leaks into later tests and produces "passes alone, fails in suite" ghosts. A sandbox registers every double in one place so afterEach restores them all and failures stay legible.

Return different values per call to cover the retry branch

const stub = sandbox.stub(client, "send").onFirstCall().throws(new Error("timeout")) stub.onSecondCall().returns({ ok: true }) // First call throws, second call returns success await service.deliver()

onCall(n) lets one stub vary per invocation, which is the only way to cover "fail first, succeed on retry". A single returns or throws can only express one outcome.

Output

{ ok: true }

Fake timers make retry backoff finish instantly

const clock = sandbox.useFakeTimers({ now: 1000 }) const promise = service.retryWithBackoff() await clock.tickAsync(3000) await promise

A real three-second backoff slows the suite and adds timing flakiness. With fake timers owning setTimeout and Date, one tick jumps to the future instantly and the outcome becomes fully deterministic.

Fake only the timer APIs you need

const clock = sandbox.useFakeTimers({ toFake: ["setTimeout", "Date"] })

By default sinon fakes requestAnimationFrame and performance too, which can stall animation or HTTP libraries. A toFake whitelist limits the takeover and avoids those hard-to-trace hangs.

Assert complex arguments with match instead of literal equality

const spy = sandbox.spy(api, "track") track("signup") sinon.assert.calledWith( spy, sinon.match({ event: "signup", props: sinon.match.number }), )

calledWith deep-compares object arguments, so a single new field breaks the assertion. sinon.match pins only the keys and types you care about, keeping assertions stable as the payload evolves.

Output

AssertError: expected spy to be called with arguments matching
  spy(event, props) { ... }

Common Pitfalls

  • A stub or spy left unrestored contaminates later tests in the same file, producing "passes alone, fails in suite" ghosts. Always go through a sandbox and restore it in afterEach.
  • A stub discards the original implementation, so stubbing a method with side effects (writes, messages) lets a test pass while production still breaks. Stub only when real behaviour is genuinely irrelevant.
  • useFakeTimers fakes setTimeout, setInterval, Date, requestAnimationFrame and performance by default; without a toFake whitelist, animation and HTTP libraries may never resolve and tests hang until timeout.
  • If the subject captured a timestamp before fake timers were installed, restoring real time skews the timeline and assertions fail for no visible reason. Check creation order before installing doubles.
  • Spying and then stubbing the same method interferes with each other: the spy records calls to the stub, not to the real behaviour. Pick one, or use sandbox.replace to do the swap in one step.
  • Assertions against real wall-clock time fail every year and in every timezone. Pin the clock with useFakeTimers({ now }) and assert against that instead.

Tips

  • Spy watches calls without changing behavior; Stub replaces behavior; Mock combines both and verifies expectations.
  • Always restore() after tests, or use a sandbox to manage automatically (call sandbox.restore in afterEach).
  • Fake Timers test scheduled tasks without real waits; remember to restore after tick.
  • sinon.assert throws errors, suited for test frameworks; you can also read spy properties directly.
  • stub.resolves/rejects handle Promises more concisely than callsFake-wrapped async functions.

FAQ

spy, stub and mock all "replace" things on an object — which should I pick?

Decide by two questions: does the behaviour change, and do you want to hard-code expectations? sinon.spy(obj, "method") wraps the real method but keeps its behaviour and only records calls, which you then read as calledOnce, calledWith(arg1, arg2), returned(value), callCount or args[0] — right for "was this called correctly". sinon.stub(obj, "method") replaces the method with a programmable empty function, discarding the original behaviour, and returns / throws / resolves / rejects decide the outcome — right for cutting a dependency off. sinon.mock(obj) combines both: declare with expects("method") including once / never and arguments, then check everything at once with mock.verify(); it is heavier, so reserve it for dense expectations.

Why do later test cases start failing once the stubbed ones pass?

Almost always the classic leak: a test double was never restored, so it leaks into everything after it. The cure shown here is the sandbox — call sinon.createSandbox() in beforeEach, create every double through sandbox.spy / sandbox.stub / sandbox.useFakeTimers, then call sandbox.restore() in afterEach to put back all spies, stubs and the fake clock in one go. The manual equivalent is calling restore() on each spy or stub individually. Fake timers are the usual offender: after clock.tick(1000), a missing clock.restore() leaves every later timing-dependent assertion frozen. Diagnostic rule: if a failing test depends on global state such as Date, setTimeout or a replaced method, go look for a leftover double in the previous test.

My timer test really waits three seconds — how do I drive a fake clock instead?

Take over setTimeout and setInterval with sinon.useFakeTimers(), then step virtual time with the clock: clock.tick(1000) advances by one second and fires only the callbacks due in that window; clock.runAll() drains every queued timer at once, which is handy when a scheduler loops. Pin the starting point with sinon.useFakeTimers({ now: 1000 }), and restrict the blast radius with sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] }) when only specific APIs should be faked. Always clock.restore() afterwards, or let the sandbox do it. The test then finishes in milliseconds while still asserting real post-callback state.

For async methods, do I wrap with callsFake or use resolves/rejects?

Prefer resolves and rejects. stub.resolves(value) hands back a resolved Promise and stub.rejects(Error) a rejected one, covering the success and failure paths of an async call far more tersely than writing an async function inside callsFake. Reach for stub.callsFake(fn) only when the replaced method does not return a Promise or when the replacement logic is genuinely multi-step. For legacy callback-style APIs use stub.callsArg(0) to invoke the first supplied argument as the callback. When successive calls must yield different values, configure them by ordinal with stub.onCall(0).returns(value).

mock.expects failures report almost nothing useful — how do I pin down what went wrong?

First establish which of three things went wrong: the expected call never happened (an expects.never() violation), or it happened the right number of times with the wrong arguments (expects.withArgs(arg) mismatch). For the second case, loosen the matcher to a structural one such as sinon.match({ id: sinon.match.number }) and constrain only the fields that matter. Also verify the received value was not polluted by a leftover test double. If strict expectation checking is not buying you anything, the mock syntax is pure overhead — switch to a spy plus assertions: sinon.assert.called, calledOnce, calledWith, notCalled, callCount(spy, 3) and calledWithExactly. Those failures point at the actual recorded call.

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.

Maintained by LaoHand

Publicly updated on Jul 21, 2026, continuously proofread against official docs.

Contact Us

Wrong command or description? Send us corrections, business inquiries or product feedback by email.

Contact Us