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.
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")sinon.spy(fn)spy.calledOncespy.calledWith(arg1, arg2)spy.returned(value)spy.callCountspy.args[0]spy.restore()Stub 9
sinon.stub(obj, "method")stub.returns(value)stub.throws(Error)stub.callsFake(fn)stub.resolves(value)stub.rejects(Error)stub.callsArg(0)stub.onCall(0).returns(value)stub.restore()Mock 7
sinon.mock(obj)mock.expects("method")expects.once()expects.withArgs(arg)expects.never()mock.verify()mock.restore()Fake Timers 6
sinon.useFakeTimers()clock.tick(1000)clock.runAll()clock.restore()sinon.useFakeTimers({ now: 1000 })sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] })Assertions & Verification 6
sinon.assert.called(spy)sinon.assert.calledOnce(spy)sinon.assert.calledWith(spy, arg)sinon.assert.notCalled(spy)sinon.assert.callCount(spy, 3)sinon.assert.calledWithExactly(spy, arg)Sandbox 5
sinon.createSandbox()sandbox.spy(obj, "method")sandbox.stub(obj, "method")sandbox.useFakeTimers()sandbox.restore()Useful Patterns 5
sinon.replace(obj, "method", fn)sinon.replaceGetter(obj, "prop", fn)sinon.replaceSetter(obj, "prop", fn)sinon.fake()sinon.match({ id: sinon.match.number })Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
sinon.spy(obj, "method") | Wrap an object method, preserving the original behavior and recording calls | sinon.spy(obj, "method") |
sinon.spy(fn) | Wrap a function as a spy, counting calls and arguments | sinon.spy(fn) |
spy.calledOnce | Check whether it was called exactly once | expect(spy.calledOnce).toBe(true) |
spy.calledWith(a, b) | Check whether it was ever called with these arguments | spy.calledWith(1, "x") |
spy.returned(value) | Check whether it ever returned the given value | spy.returned(200) |
spy.callCount | Read the cumulative call count | spy.callCount |
spy.args[0] | Get the argument array of the first call | spy.args[0][0] |
spy.restore() | Restore the wrapped original method | spy.restore() |
sinon.stub(obj, "method") | Replace the method with a programmable empty function | sinon.stub(obj, "method") |
stub.returns(value) | Return a fixed value on every call | stub.returns(42) |
stub.throws(Error) | Throw on call, to test the error branch | stub.throws(new Error("boom")) |
stub.callsFake(fn) | Replace the original function with a custom implementation | stub.callsFake(() => 1) |
stub.resolves(value) | Return a resolved Promise | stub.resolves({ id: 1 }) |
stub.rejects(Error) | Return a rejected Promise | stub.rejects(new Error("fail")) |
stub.callsArg(0) | Call the first argument as a callback, simulating callback-style APIs | stub.callsArg(0) |
stub.onCall(0).returns(v) | Return different values by call order | stub.onCall(0).returns("a") |
stub.restore() | Restore the replaced original method | stub.restore() |
sinon.mock(obj) | Create a mock object | const mock = sinon.mock(obj) |
mock.expects("method") | Expect a method to be called | mock.expects("save") |
expects.once() / never() | Expect one call / expect no call | mock.expects("save").never() |
expects.withArgs(arg) | Expect a call with the given argument | mock.expects("save").withArgs(1) |
mock.verify() | Verify all declared expectations | mock.verify() |
sinon.useFakeTimers() | Take over setTimeout / setInterval | sinon.useFakeTimers() |
clock.tick(1000) | Fast-forward the fake clock by 1000 milliseconds | clock.tick(1000) |
clock.runAll() | Drain all queued timers | clock.runAll() |
clock.restore() | Restore real time | clock.restore() |
useFakeTimers({ now }) | Set the initial time of the fake clock | sinon.useFakeTimers({ now: 1000 }) |
useFakeTimers({ toFake }) | Fake only the specified APIs | sinon.useFakeTimers({ toFake: ["setTimeout", "Date"] }) |
sinon.assert.called(spy) | Assert the spy was called | sinon.assert.called(spy) |
sinon.assert.calledOnce(spy) | Assert it was called exactly once | sinon.assert.calledOnce(spy) |
sinon.assert.calledWith(spy, a) | Assert the call passed in that argument | sinon.assert.calledWith(spy, 1) |
sinon.assert.calledWithExactly(spy, a) | Assert arguments match strictly | sinon.assert.calledWithExactly(spy, 1) |
sinon.assert.notCalled(spy) | Assert it was not called | sinon.assert.notCalled(spy) |
sinon.assert.callCount(spy, 3) | Assert the call count is 3 | sinon.assert.callCount(spy, 3) |
sinon.createSandbox() | Create an isolated sandbox to manage all doubles | const sandbox = sinon.createSandbox() |
sandbox.spy / stub / useFakeTimers | Create spies, stubs, and fake timers inside the sandbox | sandbox.stub(obj, "method") |
sandbox.restore() | Restore all doubles in the sandbox at once | sandbox.restore() |
sinon.replace(obj, "m", fn) | Replace a method, the recommended form | sinon.replace(obj, "method", fn) |
sinon.replaceGetter / replaceSetter | Replace an object getter / setter | sinon.replaceGetter(obj, "prop", fn) |
sinon.fake() | Create a fake function that does not change behavior | const fake = sinon.fake() |
sinon.match({ }) | Argument matcher that matches by type or structure | sinon.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
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.
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.
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.
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.
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.
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.
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 promiseA 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