Profiles·Public

sinon

semver>=1.0.0postconditions22functions12last verified2026-06-23coverage score80%

Postconditions: what we check

  • stub · stub-must-restore
    error
    Whensinon.stub(obj, 'method') is called
    ThrowsTypeError: Attempted to wrap [method] which is already wrapped
    Required handlingMUST call stub.restore() in afterEach or finally block to clean up stubs. Failure to restore causes "already wrapped" errors in subsequent tests. Best practice: Use sandboxes with sandbox.restore() in afterEach. Pattern: const sandbox = sinon.createSandbox(); afterEach(() => sandbox.restore());
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1]
  • stub · stub-non-existent-property
    error
    Whensinon.stub is called on non-existent or non-function property
    ThrowsTypeError: Attempted to wrap undefined property [name]
    Required handlingMUST verify property exists and is a function before stubbing. Pattern: if (typeof obj.method === 'function') { sinon.stub(obj, 'method'); }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[2]
  • stub · stub-returns-arg-invalid-index
    error
    Whenstub.returnsArg(index) is called with index >= argument count
    ThrowsTypeError: index unavailable
    Required handlingMUST ensure argument index exists before calling stub.returnsArg(index). Note: Prior to v6.1.2 returns undefined; v6.1.2+ throws TypeError.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3]
  • stub · stub-calls-arg-not-function
    error
    Whenstub.callsArg(index) is called with non-function argument
    ThrowsTypeError: index missing or not a function
    Required handlingMUST verify argument at index is a function before calling stub.callsArg(index).
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3]
  • stub · stub-yields-no-callback
    error
    Whenstub.yields() is called but stub was never called with a function argument
    ThrowsError: stub was never called with a function argument
    Required handlingMUST ensure stub is called with at least one function argument before using yields().
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3]
  • spy · spy-first-call-null-access
    error
    Whenspy.firstCall is accessed when spy was never called
    ThrowsTypeError: Cannot read property 'args' of null
    Required handlingMUST check spy.called before accessing spy.firstCall, spy.lastCall, or spy.getCall(n). Best practice: Use spy.calledWith(arg) instead of direct call access. Pattern: if (spy.called) { spy.firstCall.args }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4]
  • spy · spy-must-restore
    error
    Whensinon.spy(obj, 'method') is called
    ThrowsTypeError: Attempted to wrap [method] which is already wrapped
    Required handlingMUST call spy.restore() in afterEach or finally block. Use sandboxes for automatic cleanup: sandbox.spy(obj, 'method'); afterEach(() => sandbox.restore());
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[5]
  • mock · mock-verify-not-called
    error
    Whenmock.expects() is called but mock.verify() is never called
    ThrowsSilent failure - expectations not enforced
    Required handlingMUST call mock.verify() or sandbox.verifyAndRestore() to enforce expectations. Without verification, tests pass even when expectations are not met. Pattern: afterEach(() => { mock.verify(); }) or sandbox.verifyAndRestore()
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6]
  • mock · mock-verify-fails
    info
    Whenmock.verify() is called and expectations are not met
    ThrowsExpectationError
    Required handlingThis is expected behavior when expectations fail. Ensure expectations match actual usage.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6]
  • useFakeTimers · fake-timers-must-restore
    error
    Whensinon.useFakeTimers() is called
    ThrowsTest hangs, setTimeout/setInterval never fire in subsequent tests
    Required handlingMUST call clock.restore() in afterEach or finally block. Failure to restore causes timer pollution and test hangs. Pattern: let clock; beforeEach(() => clock = sinon.useFakeTimers()); afterEach(() => clock.restore());
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7]
  • createSandbox · sandbox-must-restore
    error
    Whensinon.createSandbox() is called
    ThrowsTest pollution, 'already wrapped' errors in subsequent tests
    Required handlingMUST call sandbox.restore() in afterEach or finally block. Sandboxes simplify cleanup but still require explicit restoration. Pattern: const sandbox = sinon.createSandbox(); afterEach(() => sandbox.restore());
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8]
  • createStubInstance · create-stub-instance-double-use
    error
    Whensinon.createStubInstance is called twice on same constructor without cleanup
    ThrowsTypeError: Attempted to wrap [method] which is already wrapped
    Required handlingMUST restore or use different sandbox between createStubInstance calls. Pattern: Use sandbox.createStubInstance() and restore sandbox between uses.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[9]
  • fake · fake-non-function-arg-throws
    error
    Whensinon.fake(value) is called where value is not a function (e.g. a string, number, plain object, or undefined passed explicitly as first argument). sinon.fake() with zero arguments is valid; sinon.fake(someString) throws.
    ThrowsTypeError: Expected f argument to be a Function
    Required handlingMUST pass either no argument (blank fake) or a Function when calling sinon.fake(). Sub-methods (fake.returns, fake.resolves, fake.rejects, etc.) accept non-function values and are the correct API when you want a preset return value. // Correct: blank fake const f = sinon.fake(); // Correct: wrap an existing function const f2 = sinon.fake(myImpl); // Correct: preset return value via factory const f3 = sinon.fake.returns(42); // WRONG: passing a non-function const f4 = sinon.fake("not a function"); // TypeError!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[10][11]
  • fake · fake-yields-missing-callback
    error
    WhenA fake created with sinon.fake.yields(...values) or sinon.fake.yieldsAsync(...values) is invoked at call time without a function as its last argument. The TypeError is thrown when the fake is called, not when it is created.
    ThrowsTypeError: Expected last argument to be a function
    Required handlingMUST ensure the last argument passed to a yields/yieldsAsync fake is a function. This is the callback convention: the fake expects to invoke its last argument. // Correct: caller always passes a callback last const fake = sinon.fake.yields("result"); fake("arg1", (result) => console.log(result)); // ok // WRONG: no callback fake("arg1"); // TypeError at invocation time!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[10][11]
  • replace · replace-non-existent-property
    error
    Whensandbox.replace(object, property, replacement) is called where `property` does not exist on `object` or its prototype chain. A common mistake when refactoring code that removes or renames properties while tests still reference old names.
    ThrowsTypeError: Cannot replace non-existent property '<property>'. Perhaps you meant sandbox.define()?
    Required handlingMUST verify the property exists on the object before calling sandbox.replace(). If the goal is to add a new property (not replace an existing one), use sandbox.define(object, property, value) instead. // Correct: property exists const obj = { method: () => 'original' }; sandbox.replace(obj, 'method', sandbox.stub()); // ok // WRONG: property doesn't exist sandbox.replace(obj, 'nonExistent', sandbox.stub()); // TypeError!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8][12]
  • replace · replace-already-replaced-throws
    error
    Whensandbox.replace(object, property, replacement) is called on a property that has already been replaced via sandbox.replace() in the same sandbox without an intervening sandbox.restore(). Happens when the same setup code is called twice, or two beforeEach hooks both replace the same property.
    ThrowsTypeError: Attempted to replace '<property>' which is already replaced
    Required handlingMUST call sandbox.restore() between replace calls on the same property. Use a single sandbox per test scope and restore in afterEach. // Correct: one replace per sandbox lifecycle beforeEach(() => { sandbox.replace(obj, 'method', replacement); }); afterEach(() => { sandbox.restore(); // clears the replacement }); // WRONG: replacing twice without restore sandbox.replace(obj, 'method', stub1); // ok sandbox.replace(obj, 'method', stub2); // TypeError: already replaced!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8][12]
  • replace · replace-accessor-property-throws
    error
    Whensandbox.replace(object, property, replacement) is called on a property that is defined as a getter or setter (accessor property). sandbox.replace() only works on value properties (data descriptors). Attempting to replace a getter or setter throws an Error directing to use replaceGetter/replaceSetter.
    ThrowsError: Use sandbox.replaceGetter for replacing getters
    Required handlingUse sandbox.replaceGetter() for getter properties, sandbox.replaceSetter() for setter properties, or sandbox.replace.usingAccessor() when you need to replace the underlying value of an accessor property. // Correct: use replaceGetter for getters sandbox.replaceGetter(obj, 'computedProp', () => 'test-value'); // WRONG: using replace() on a getter // if 'computedProp' is a getter: sandbox.replace(obj, 'computedProp', 'test-value'); // Error!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8][12]
  • replace · replace-type-mismatch-throws
    error
    Whensandbox.replace(object, property, replacement) is called where the replacement is a different JavaScript type than the original property value. For example, replacing a function property with a plain object, or replacing a string property with a number. sinon enforces type consistency to prevent accidental misuse.
    ThrowsTypeError: Cannot replace <originalType> with <replacementType>
    Required handlingMUST pass a replacement of the same type as the original property. When replacing a function, use a stub/spy/fake (which are functions). When replacing a primitive, use the same primitive type. const obj = { value: 'original-string' }; // Correct: same type sandbox.replace(obj, 'value', 'replacement-string'); // ok // WRONG: different type sandbox.replace(obj, 'value', 42); // TypeError: Cannot replace string with number!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8][12]
  • define · define-existing-property-throws
    error
    Whensandbox.define(object, property, value) is called where `property` already exists as an own property on `object`. Common mistake when test setup code reuses the same property name across tests, or when refactoring stub() → define() without checking whether the property pre-exists. Also fires when `property` is undefined (e.g. sandbox.define(obj, obj.missingKey, val) where missingKey resolves to undefined).
    ThrowsTypeError: Cannot define the already existing property '<property>'. Perhaps you meant sandbox.replace()?
    Required handlingMUST use sandbox.replace() (or sandbox.stub() for functions) when the property already exists on the object. Use sandbox.define() only to add NEW properties that need to be tracked for cleanup. const obj = { existingMethod: () => 'original' }; // Correct: define a NEW property (does not exist yet) sandbox.define(obj, 'newMethod', sandbox.stub().returns('test')); // Correct: replace an EXISTING property sandbox.replace(obj, 'existingMethod', sandbox.stub().returns('test')); // WRONG: define on an existing property sandbox.define(obj, 'existingMethod', sandbox.stub()); // TypeError!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[8][13]
  • verify · verify-unmet-expectation-throws
    error
    Whensandbox.verify() is called and at least one registered mock has an unmet expectation (e.g. expects(method).withArgs(X) was set but the method was never called with X, or was called fewer/more times than expects(method).once()/atLeast()/exactly()). This is the canonical 'mock verification failed' signal — typical in test assertions but DANGEROUS in production code or test cleanup hooks that swallow the error and let subsequent tests pollute the state.
    ThrowsExpectationError: expected <method> to be called with arguments matching ... but was called <n> times
    Required handlingWhen sandbox.verify() is called outside a test framework's explicit assertion phase (e.g. in afterEach where the test result has already been reported), the caller MUST wrap in try/catch to attach the verification failure to the right test, AND MUST still call sandbox.restore() in a finally block to clean up the sandbox state. Otherwise the ExpectationError escapes into the test framework, subsequent tests run against a polluted sandbox, and the original failing test may already be marked passing. Best practice: use sandbox.verifyAndRestore() instead — it wraps the verify+ restore pattern correctly: // Correct: verifyAndRestore handles try/finally for you afterEach(() => { sandbox.verifyAndRestore(); }); // Manual verify path requires explicit try/finally afterEach(() => { try { sandbox.verify(); } finally { sandbox.restore(); } }); // WRONG: bare verify() — if verification fails, restore() is skipped afterEach(() => { sandbox.verify(); sandbox.restore(); // never reached if verify throws! });
    costmediumin prodsilent failureusers seeservice unavailablevisibilitysilent
    Sources[8][6][13]
  • verifyAndRestore · verify-and-restore-expectation-throws
    error
    Whensandbox.verifyAndRestore() is called and at least one registered mock has an unmet expectation. Unlike bare verify(), the sandbox IS guaranteed restored before ExpectationError propagates — but the test framework still receives the throw, so the caller must let it bubble to a test failure or catch it.
    ThrowsExpectationError: expected <method> to be called with arguments matching ... but was called <n> times
    Required handlingIn test afterEach hooks, allow the ExpectationError to propagate so the test framework can record the failure. The sandbox is guaranteed restored, so no finally block is required. Production code calling verifyAndRestore() (e.g. mock-driven integration tests run in CI) MUST allow the throw to surface or wrap in try/catch to record the assertion failure; swallowing it silently means failing mocks pass the test suite undetected. // Correct: afterEach lets the error reach the test framework afterEach(() => { sandbox.verifyAndRestore(); }); // Correct: explicit catch records the failure try { sandbox.verifyAndRestore(); } catch (e) { reporter.recordFailure(e); throw e; } // WRONG: silent swallow — mock failures pass undetected try { sandbox.verifyAndRestore(); } catch {}
    costmediumin prodsilent failureusers seeservice unavailablevisibilitysilent
    Sources[8][13]
  • restoreObject · restore-object-falsy-input-throws
    error
    Whensinon.restoreObject(object) is called with a falsy value: null, undefined, 0, false, NaN, or ''. Common in test teardown code that loops over a collection of stubbed targets and forgets to filter out already-cleared references, or where a teardown helper receives an optional object that is sometimes absent.
    ThrowsError: Trying to restore object but received <stringified-falsy-value>
    Required handlingMUST guard with a truthy check before calling sinon.restoreObject(), or wrap the call in try/catch in cleanup loops that may legitimately encounter already-cleared references. // Correct: guard if (target) { sinon.restoreObject(target); } // Correct: defensive try-catch in a cleanup loop for (const target of targets) { try { sinon.restoreObject(target); } catch { /* already cleared */ } } // WRONG: no guard sinon.restoreObject(maybeUndefined); // throws Error!
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[14][15]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Official documentation
  • [3]
    sinonjs.org/releases/latest/stubs
    Stubs
  • [6]
    sinonjs.org/releases/latest/mocks
    Mocks
  • [7]
    sinonjs.org/releases/latest/fake-timers
    Fake Timers
  • [8]
    sinonjs.org/releases/latest/sandbox
    Sandbox
  • [10]
    sinonjs.org/releases/latest/fakes
    Fakes
  • [14]
    sinonjs.org/releases/latest/utils
    Utils
Source code
Issues & pull requests

Research notes

Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.

Sources: sinon Package Contract

Package: sinon Version Range: >=1.0.0 Last Updated: 2026-02-27


Overview

Sinon.JS is a standalone test double library for JavaScript that provides spies, stubs, and mocks with no dependencies. It works with any unit testing framework. The library enables developers to test asynchronous code, control time, and verify function behaviors without relying on external services or complex setup.

Key Features:

  • Spies: Track function calls, arguments, return values, and exceptions
  • Stubs: Replace functions with controlled behavior for testing
  • Mocks: Pre-programmed expectations with built-in assertions
  • Fake Timers: Control setTimeout, setInterval, Date, and process.nextTick
  • Fake XHR/Servers: Control AJAX requests and HTTP responses
  • Sandboxes: Simplified cleanup and state management

Primary Error Patterns

1. Missing Restore Calls

Problem: Stubs, spies, and fake timers that are not restored cause test pollution and "already wrapped" errors in subsequent tests.

Common Manifestations:

  • TypeError: "Attempted to wrap [method] which is already wrapped"
  • Tests fail when run together but pass in isolation
  • Stub behavior persists across test boundaries
  • Memory leaks from unreleased test doubles

Documentation Reference:

"Sandboxes remove the need to keep track of every fake created, which greatly simplifies cleanup." — Sinon Sandbox Documentation

"Restore the faked methods." — Sinon Fake Timers Documentation

Best Practice Pattern:

// Using sandboxes (recommended)
const sandbox = sinon.createSandbox();

beforeEach(() => {
  sandbox.stub(obj, 'method');
});

afterEach(() => {
  sandbox.restore(); // REQUIRED - prevents test pollution
});

// Manual cleanup (less recommended)
let stub: sinon.SinonStub;

beforeEach(() => {
  stub = sinon.stub(obj, 'method');
});

afterEach(() => {
  stub.restore(); // REQUIRED
});

// Fake timers cleanup (critical)
let clock: sinon.SinonFakeTimers;

beforeEach(() => {
  clock = sinon.useFakeTimers();
});

afterEach(() => {
  clock.restore(); // REQUIRED - prevents timer pollution
});

Real-World Impact: This is the most common Sinon error, affecting an estimated 30-40% of test suites that use manual cleanup instead of sandboxes.


2. Double-Stubbing Without Restore

Problem: Attempting to stub a method that is already stubbed throws a TypeError.

Error Message:

TypeError: Attempted to wrap [method] which is already wrapped

GitHub Issues:

  • Issue #1673: "Attempted to wrap send which is already wrapped" occurs when running multiple test files
  • Issue #1682: Using sinon.stub(redis) throws "Attempted to wrap getBuiltinCommands which is already wrapped"
  • Issue #1775: Default sandbox's restore method does not restore stubs properly, causing subsequent stubs to fail
  • Issue #852: sinon.createStubInstance cannot be used twice on the same constructor
  • Issue #1721: Multiple reports of "Attempted to wrap [...] which is already wrapped"

Root Cause: Sinon tracks wrapped methods and prevents double-wrapping to avoid undefined behavior. When restore() is not called, or when the restore mechanism fails, subsequent stub attempts trigger this error.

Prevention:

// BAD - double-stubbing
sinon.stub(obj, 'method').returns(1);
sinon.stub(obj, 'method').returns(2); // TypeError

// GOOD - restore first
const stub1 = sinon.stub(obj, 'method').returns(1);
stub1.restore();
const stub2 = sinon.stub(obj, 'method').returns(2);
stub2.restore();

// BETTER - use sandboxes
const sandbox = sinon.createSandbox();
sandbox.stub(obj, 'method').returns(1);
sandbox.restore(); // Cleans up all stubs
sandbox.stub(obj, 'method').returns(2);

3. Accessing Spy Calls Without Checking Existence

Problem: Accessing spy.firstCall, spy.lastCall, or spy.getCall(n) when the spy has not been called results in null/undefined access errors.

Error Pattern:

const spy = sinon.spy();
// spy is never called
console.log(spy.firstCall.args); // TypeError: Cannot read property 'args' of null

GitHub Issues:

  • Issue #1476: stub.firstCall returns null when using withArgs(), even though getCall(0) works
  • Issue #1487: Incorrect returnValue when using withArgs() with firstCall or lastCall
  • Issue #1936: Default spy properties include firstCall: null

Documentation Note:

"The recommended approach is going with spy.calledWith(arg1, arg2, ...) rather than direct array access, as this keeps tests less brittle." — Sinon Spies Documentation

Safe Pattern:

const spy = sinon.spy();

// BAD - no existence check
if (spy.firstCall.args[0] === 'value') { ... } // May throw

// GOOD - check called first
if (spy.called && spy.firstCall.args[0] === 'value') { ... }

// BETTER - use assertion methods
if (spy.calledWith('value')) { ... }

// BEST - use sinon assertions
sinon.assert.calledWith(spy, 'value');

4. Mock Expectations Not Verified

Problem: Mocks with expectations that are never verified silently fail to enforce test assertions.

Impact: Tests pass even when expected behavior doesn't occur, resulting in false confidence.

Documentation Reference:

"Verifies all expectations on the mock. If any expectation is not satisfied, an exception is thrown." — Sinon Mocks Documentation

Pattern:

// BAD - expectations never verified
const mock = sinon.mock(obj);
mock.expects('method').once();
obj.method(); // May or may not be called
// Test passes even if method wasn't called

// GOOD - verify expectations
const mock = sinon.mock(obj);
mock.expects('method').once();
obj.method();
mock.verify(); // Throws if expectation not met

// BETTER - use sandbox for automatic verification
const sandbox = sinon.createSandbox();
const mock = sandbox.mock(obj);
mock.expects('method').once();
obj.method();
sandbox.verifyAndRestore(); // Verifies and cleans up

Best Practice Guidance:

"In general you should have no more than one mock (possibly with several expectations) in a single test." — Sinon Mocks Best Practices


5. Fake Timer Cleanup Failures

Problem: Fake timers that are not restored cause time-based tests to behave unexpectedly and can break unrelated tests.

Symptoms:

  • Tests hang waiting for real timers that never fire
  • setTimeout/setInterval in subsequent tests execute immediately or never
  • Date.now() returns frozen time in tests that should use real time

Documentation Warning:

"Call clock.restore() in tearDown to restore the faked methods." — Sinon Fake Timers Documentation

"When faking nextTick, normal calls to process.nextTick() will not execute automatically. You must manually invoke clock.next(), clock.tick(), clock.runAll(), or clock.runToLast()." — Sinon Fake Timers: Critical Considerations

Pattern:

// BAD - no timer cleanup
it('test with fake timers', () => {
  const clock = sinon.useFakeTimers();
  // ... test code
  // Missing: clock.restore()
});

it('subsequent test', () => {
  setTimeout(() => console.log('fired'), 100); // Never fires!
});

// GOOD - proper cleanup
let clock: sinon.SinonFakeTimers;

beforeEach(() => {
  clock = sinon.useFakeTimers();
});

afterEach(() => {
  clock.restore(); // Critical for test isolation
});

// BETTER - use sandboxes
const sandbox = sinon.createSandbox({ useFakeTimers: true });

afterEach(() => {
  sandbox.restore(); // Restores timers automatically
});

6. Stubbing Non-Existent or Non-Function Properties

Problem: Attempting to stub a property that doesn't exist or is not a function throws a TypeError.

Error Messages:

TypeError: Attempted to wrap undefined property [name]
TypeError: An exception is thrown if the property is not already a function

GitHub Issues:

  • Issue #1762: "Attempted to wrap undefined property" when using .spy() on non-existent properties
  • Issue #470: "Fails with unrelated error when non-existent method stubbed"

Documentation Reference:

"An exception is thrown if the property is not already a function" — Sinon Stubs Documentation

Pattern:

const obj = { existingMethod: () => {} };

// BAD - stubbing non-existent property
sinon.stub(obj, 'nonExistent'); // TypeError

// BAD - stubbing non-function
const obj2 = { prop: 'value' };
sinon.stub(obj2, 'prop'); // TypeError

// GOOD - verify property exists and is a function
if (typeof obj.method === 'function') {
  sinon.stub(obj, 'method');
}

// BETTER - use type-safe approach
interface TestObj {
  method: () => void;
}
const obj: TestObj = { method: () => {} };
sinon.stub(obj, 'method'); // Type-checked

7. Stub Configuration Errors

Problem: Invalid stub configurations throw TypeErrors at runtime.

Common Issues:

7a. Invalid Argument Index Access:

// stub.returnsArg(index) - TypeErrors for invalid index
const stub = sinon.stub();
stub.returnsArg(5); // No error until called with fewer args

stub(1, 2); // TypeError: index 5 unavailable

Documentation:

"Prior to v6.1.2, returns undefined if index unavailable; v6.1.2+ throws TypeError" — Sinon Stubs: stub.returnsArg()

7b. Invalid Callback Index:

// stub.callsArg(index) - TypeError if not a function
const stub = sinon.stub();
stub.callsArg(0);

stub('not a function'); // TypeError: index missing or not a function

7c. Missing Callback for yield():

const stub = sinon.stub();
stub.yields('arg1', 'arg2');

stub(); // Error: stub was never called with a function argument

Documentation:

"If the stub was never called with a function argument, yield throws an error" — Sinon Stubs: stub.yields()


8. Restoration Failures with Special Properties

Problem: Certain properties cannot be properly stubbed or restored, particularly in browser environments.

GitHub Issues:

  • Issue #1881: In IE11, stubbing window properties throws "Cannot redefine non-configurable property"
  • Issue #2226: Restoring stub fails when it executed .value() - throws TypeError
  • Issue #714: Restoring spies/stubs of prototype functions of Element fails
  • Issue #2384: sandbox.restore() does not work on static members of functions/classes

Specific Case - IE11 Window Properties:

"In IE11, it's possible to stub but not restore some properties in the window object, causing sinon to throw a TypeError: Cannot redefine non-configurable property" — Issue #1881

Specific Case - .value() Stubs:

"When a stub is created with .value() method (like sinon.stub(mongoose.connection, 'readyState').value(1)), calling sinon.restore() throws a TypeError" — Issue #2226

Workarounds:

// Check configurability before stubbing
if (Object.getOwnPropertyDescriptor(obj, 'prop')?.configurable) {
  sinon.stub(obj, 'prop');
}

// Avoid .value() stubs if restoration is needed
// Instead, use property accessors or replace the entire object

9. Async Test Cleanup Timing Issues

Problem: Sandbox restoration occurs before async tests complete, causing unpredictable behavior.

GitHub Issue:

  • Issue #1119: "sinon.test restores the sandbox before a promise-based async test is completed"

Pattern:

// BAD - restoration races with async code
it('async test', async () => {
  const stub = sinon.stub(obj, 'method');
  const promise = asyncFunction(); // Uses stub
  stub.restore(); // Restored before promise resolves!
  await promise; // May fail
});

// GOOD - await before restore
it('async test', async () => {
  const stub = sinon.stub(obj, 'method');
  const result = await asyncFunction(); // Complete before restore
  stub.restore();
  return result;
});

// BETTER - use afterEach for cleanup
let stub: sinon.SinonStub;

beforeEach(() => {
  stub = sinon.stub(obj, 'method');
});

afterEach(() => {
  stub.restore(); // Runs after test completes
});

it('async test', async () => {
  return asyncFunction(); // Stub still active
});

10. Spy/Stub Conflicts with ES6 Classes

Problem: Stubbing ES6 class instances and static members has special behavior that can cause "already wrapped" errors.

GitHub Issues:

  • Issue #878: "Stubbing and restoring do not work with ES6 class instances"
  • Issue #2029: "Separate stubs for class and its instance"
  • Issue #867: "Prototype chain and stub breaking behaviors"

Pattern:

class MyClass {
  method() { return 'original'; }
  static staticMethod() { return 'static'; }
}

// BAD - stubbing instance after createStubInstance
const instance = sinon.createStubInstance(MyClass);
sinon.stub(instance, 'method'); // TypeError: already wrapped

// GOOD - configure stubs during creation
const instance = sinon.createStubInstance(MyClass, {
  method: sinon.stub().returns('stubbed')
});

// BAD - static member restoration issues
sinon.stub(MyClass, 'staticMethod');
// ... may not restore properly with sandbox

// BETTER - use sandbox for all stubs
const sandbox = sinon.createSandbox();
sandbox.stub(MyClass, 'staticMethod');
sandbox.restore(); // More reliable for static members

Stub API Reference

Creation Methods

Anonymous Stub:

const stub = sinon.stub();

Creates a standalone stub function with no behavior.

Stub Object Method:

const stub = sinon.stub(object, "method");

Replaces object.method with a stub. Original function is not called.

Restoration: Must call object.method.restore() or stub.restore() to restore original.

Stub All Methods:

const stub = sinon.stub(obj);

Stubs all methods on an object.

Documentation Warning:

"It's usually better practice to stub individual methods to test intent precisely." — Sinon Stubs: Best Practices

Create Stub Instance:

const stub = sinon.createStubInstance(MyConstructor, overrides);

Creates a stub instance without invoking the constructor.


Behavior Configuration

Return Values:

  • stub.returns(obj) - Returns specified value
  • stub.returnsArg(index) - Returns argument at index (throws TypeError if unavailable in v6.1.2+)
  • stub.returnsThis() - Returns this context (for fluent APIs)

Promises:

  • stub.resolves(value) - Returns Promise resolving to value
  • stub.resolvesArg(index) - Returns Promise resolving to argument (throws TypeError if unavailable)
  • stub.rejects() - Returns rejected Promise
  • stub.rejects("TypeError") - Returns Promise rejected with typed exception

Exceptions:

  • stub.throws() - Throws generic Error
  • stub.throws("name"[, "message"]) - Throws named exception
  • stub.throws(obj) - Throws provided exception object
  • stub.throwsArg(index) - Throws argument as exception (throws TypeError if unavailable)

Callbacks:

  • stub.callsArg(index) - Invokes argument as callback (throws TypeError if not a function)
  • stub.callsArgWith(index, arg1, arg2) - Invokes with arguments
  • stub.yields([arg1, arg2]) - Calls first function argument
  • stub.yieldsTo(property, [args]) - Invokes property callback

Async Callbacks: All callback methods have async variants that defer using process.nextTick (Node) or setTimeout(callback, 0) (browser):

  • callsArgAsync(), callsArgWithAsync(), yieldsAsync(), etc.

Conditional Stubs:

stub.withArgs(arg1, arg2).returns(value);

Stubs method only for specific arguments. Uses deep comparison by default.

Sequential Behavior:

stub.onCall(n); // Configure behavior for nth call
stub.onFirstCall();  // onCall(0)
stub.onSecondCall(); // onCall(1)
stub.onThirdCall();  // onCall(2)

Custom Behavior:

stub.callsFake(fakeFunction);

Executes provided function when stub is invoked.

Call Through:

stub.callThrough();

Calls original wrapped method when conditional stubs don't match.


State Management

Reset Methods:

  • stub.reset() - Resets behavior and call history
  • stub.resetBehavior() - Resets to default behavior only
  • stub.resetHistory() - Clears call history only

Restoration:

  • stub.restore() - CRITICAL: Restores original method

Batch Operations:

sinon.reset();          // All stubs
sinon.resetBehavior();  // All stubs
sinon.resetHistory();   // All stubs

Spy API Reference

Creation Methods

  1. Anonymous Spy:
const spy = sinon.spy();
  1. Function Wrapper:
const spy = sinon.spy(myFunc);
  1. Method Spy:
const spy = sinon.spy(object, "method");
  1. Property Accessor Spy:
const spy = sinon.spy(object, "property", ["get", "set"]);

Call Information Properties

Call Counters:

  • spy.callCount - Total number of calls
  • spy.called - Boolean: at least one call
  • spy.notCalled - Boolean: no calls
  • spy.calledOnce, spy.calledTwice, spy.calledThrice - Exact counts

Call Access (may be null if not called):

  • spy.firstCall - First call object (null if never called)
  • spy.secondCall - Second call object
  • spy.thirdCall - Third call object
  • spy.lastCall - Last call object (null if never called)
  • spy.getCall(n) - Get nth call (supports negative indexing)
  • spy.getCalls() - Array of all call objects

Call Data Arrays:

  • spy.args[n] - Arguments array for nth call
  • spy.thisValues[n] - Context objects for each call
  • spy.exceptions[n] - Exception data (undefined if no error)
  • spy.returnValues[n] - Return values (undefined if none)

Verification Methods

Argument Matching:

  • spy.calledWith(arg1, arg2) - Checks if called with provided arguments
  • spy.calledWithExactly(...) - Exact argument match
  • spy.calledOnceWith(...) - Exactly one call with arguments
  • spy.alwaysCalledWith(...) - All calls match arguments
  • spy.calledWithMatch(...) - Matcher-based comparison

Context Verification:

  • spy.calledOn(obj) - Verifies this context
  • spy.alwaysCalledOn(obj) - All calls used target context
  • spy.calledWithNew() - Detects constructor invocation

Call Ordering:

  • spy.calledBefore(anotherSpy) - Temporal ordering
  • spy.calledAfter(anotherSpy) - Reverse ordering
  • spy.calledImmediatelyBefore(anotherSpy) - Sequential calls
  • spy.calledImmediatelyAfter(anotherSpy) - Immediate successor

Exception/Return Tracking:

  • spy.threw() - Returns true if spy threw exception
  • spy.threw("TypeError") - Specific exception type check
  • spy.alwaysThrew() - All invocations threw
  • spy.returned(obj) - Verifies returned value
  • spy.alwaysReturned(obj) - Consistent return verification

Best Practice:

"The recommended approach is going with spy.calledWith(arg1, arg2, ...) rather than direct array access, as this keeps tests less brittle." — Sinon Spies Documentation


Restoration

spy.restore(); // Only for wrapped methods
spy.resetHistory(); // Clear call history

Mock API Reference

Creation and Expectations

Create Mock:

const mock = sinon.mock(obj);

Returns a mock object for setting expectations. Does not change the original object.

Set Expectation:

const expectation = mock.expects("method");

Overrides the method with a mock function and returns an expectation object.


Expectation Methods (Chainable)

Call Count Expectations:

  • expectation.atLeast(number) - Minimum call count
  • expectation.atMost(number) - Maximum call count
  • expectation.exactly(number) - Exact call count
  • expectation.never() - Method should never be called
  • expectation.once() - Exactly one call
  • expectation.twice() - Exactly two calls
  • expectation.thrice() - Exactly three calls

Argument Expectations:

  • expectation.withArgs(arg1, arg2) - Called with these args (and possibly others)
  • expectation.withExactArgs(arg1, arg2) - Called with only these exact args

Context Expectations:

  • expectation.on(obj) - Called with specific this context

Important Limitation:

"An expectation instance only holds onto a single set of arguments specified with withArgs or withExactArgs—subsequent calls overwrite previous specifications." — Sinon Mocks Documentation


Verification

Verify All Expectations:

mock.verify();

Verifies all expectations on the mock. Throws an exception if any expectation is not satisfied. Also restores the mocked methods.

Verify Individual Expectation:

expectation.verify();

Consequence of Unmet Expectations:

"A mock will fail your test if it is not used as expected." — Sinon Mocks Documentation


Best Practices for Mocks

Limit Mock Usage:

"In every unit test, there should be one unit under test. In general you should have no more than one mock (possibly with several expectations) in a single test." — Sinon Mocks Best Practices

Avoid Over-Specification:

"If you wouldn't add an assertion for some specific call, don't mock it. Use a stub instead. Mocks come with built-in expectations that may fail your test. Thus, they enforce implementation details." — Sinon Mocks Best Practices

When to Use Mocks: Employ mocks "if you want to control how your unit is being used and like stating expectations upfront."


Sandbox API Reference

Creation

Basic Sandbox:

const sandbox = sinon.createSandbox();

Configured Sandbox:

const sandbox = sinon.createSandbox({
  useFakeTimers: true,
  injectInto: facadeObject,
  properties: ["spy", "stub"]
});

Default Sandbox (Sinon 5+):

"The sinon object itself functions as a default sandbox, eliminating the need for manual sandbox creation in most scenarios." — Sinon Sandbox Documentation


Sandbox Methods

All standard Sinon methods are available on sandboxes:

  • sandbox.stub()
  • sandbox.spy()
  • sandbox.mock()
  • sandbox.useFakeTimers()
  • sandbox.createStubInstance()
  • sandbox.replace(), sandbox.replaceGetter(), sandbox.replaceSetter()

Cleanup Methods

Primary Methods:

  • sandbox.restore() - CRITICAL: Restores all fakes completely
  • sandbox.reset() - Resets internal state of all fakes
  • sandbox.resetHistory() - Clears call history only
  • sandbox.resetBehavior() - Resets stub behaviors
  • sandbox.verify() - Validates mock expectations
  • sandbox.verifyAndRestore() - Both verification and cleanup

Best Practice Integration:

describe("myAPI.hello", function() {
  const sandbox = sinon.createSandbox();

  beforeEach(() => {
    sandbox.stub(myAPI, "hello");
  });

  afterEach(() => {
    sandbox.restore(); // REQUIRED
  });

  it("should be called once", () => {
    // test code
  });
});

Benefit:

"Sandboxes remove the need to keep track of every fake created, which greatly simplifies cleanup." — Sinon Sandbox Documentation


Fake Timers API Reference

Creation

Basic Creation (starts at Unix epoch):

const clock = sinon.useFakeTimers();

With Specific Timestamp:

const clock = sinon.useFakeTimers(now); // number or Date object

With Configuration:

const clock = sinon.useFakeTimers({
  now: timestamp,
  toFake: ["setTimeout", "nextTick"], // Specific functions to fake
  shouldAdvanceTime: true, // Auto-advance based on system time
  global: globalObject // For Node/JSDOM environments
});

Clock Control Methods

Synchronous Methods:

  • clock.tick(time) - Advances clock by milliseconds; accepts "08" (8s) or "02:34:10" formats
  • clock.jump(time) - Skips forward, fires callbacks at most once (simulates sleep/resume)
  • clock.next() - Advances to the first scheduled timer
  • clock.runAll() - Executes all pending timers until none remain

Asynchronous Methods (for Promise-based code):

  • await clock.tickAsync(time) - Ticks while breaking event loop for promise execution
  • await clock.nextAsync() - Advances asynchronously
  • await clock.runAllAsync() - Runs all timers asynchronously

Restoration (CRITICAL)

clock.restore();

Documentation Warning:

"Call clock.restore() in tearDown to restore the faked methods." — Sinon Fake Timers Documentation

Consequence of Missing Restoration: Fake timers persist across tests, causing:

  • setTimeout/setInterval never fire in subsequent tests
  • Date.now() returns frozen time
  • process.nextTick() doesn't execute
  • Tests hang waiting for real timers

Critical Gotchas

Process.nextTick Behavior:

"When faking nextTick, normal calls to process.nextTick() will not execute automatically. You must manually invoke clock.next(), clock.tick(), clock.runAll(), or clock.runToLast()." — Sinon Fake Timers Documentation

Async/Await Pattern:

"Using await on async functions without advancing the clock causes hangs; instead, call the function without awaiting, then use await clock.tickAsync()." — Sinon Fake Timers Documentation

Correct Pattern:

// BAD - hangs
await asyncFunction();
await clock.tick(1000);

// GOOD - fire and tick
asyncFunction(); // Don't await
await clock.tickAsync(1000);

Common Vulnerability Patterns

Test Pollution

Issue: Missing restore() calls cause test state to leak between tests.

Impact:

  • Tests fail when run together but pass in isolation
  • "Already wrapped" TypeErrors
  • Flaky test suites
  • False positives/negatives

Prevention:

// Always use afterEach cleanup
afterEach(() => {
  sandbox.restore();
  clock.restore();
  sinon.restore(); // If using default sandbox
});

Memory Leaks

Issue: Unreleased stubs, spies, and mocks accumulate in memory.

Impact:

  • Increasing memory consumption over test runs
  • Slower test execution
  • Eventual out-of-memory errors in large suites

Prevention: Use sandboxes for automatic cleanup management.

Configuration:

const sandbox = sinon.createSandbox({
  assertOptions: {
    shouldLimitAssertionLogs: true,
    assertionLogLimit: 100
  }
});

Race Conditions in Async Tests

Issue: Stubs restored before async code completes.

Impact: Unpredictable test failures, intermittent errors.

Prevention:

  • Always await async operations before cleanup
  • Use afterEach for cleanup, not inline restore()
  • Avoid manual restore in the middle of async tests

Severity Levels

ERROR (Must Fix)

  1. Missing restore() on stubs/spies/mocks

    • Causes test pollution
    • Blocks subsequent tests
    • TypeErrors: "already wrapped"
  2. Missing clock.restore() on fake timers

    • Breaks all subsequent tests using timers
    • Causes test hangs
    • Tests may never complete
  3. Missing mock.verify()

    • Expectations silently not enforced
    • False confidence in test coverage
    • Tests pass when they should fail
  4. Accessing spy calls without existence checks

    • TypeError: Cannot read property 'args' of null
    • Runtime test failures
    • Brittle tests

WARNING (Should Fix)

  1. Not using sandboxes for complex test suites

    • Increased risk of cleanup errors
    • More maintenance overhead
    • Harder to track all fakes
  2. Over-using mocks instead of stubs

    • Brittle tests tied to implementation
    • Harder to refactor
    • False failures on benign changes
  3. Stubbing entire objects instead of individual methods

    • Less precise test intent
    • Potential for unexpected side effects
    • Harder to debug failures

Version-Specific Behavior

Sinon v6.1.2+

Breaking Change:

"Prior to v6.1.2, returns undefined if index unavailable; v6.1.2+ throws TypeError" — Sinon Stubs: stub.returnsArg()

Methods affected:

  • stub.returnsArg(index)
  • stub.resolvesArg(index)
  • stub.throwsArg(index)
  • stub.callsArg(index)

Migration: Add bounds checking when accessing arguments by index.

Sinon v5.0+

Default Sandbox: The sinon object itself became a default sandbox, reducing boilerplate for most use cases.

Sinon v2.x → v3.x

Prototype Changes: Issue #867 documents breaking changes in prototype chain handling for stubs.


Additional Resources

Official Documentation:

GitHub:

NPM:


Summary

Sinon.JS test doubles are powerful tools for isolating unit tests, but they require careful cleanup management to avoid test pollution. The most common bugs stem from missing restore() calls on stubs, spies, and fake timers, leading to "already wrapped" TypeErrors and test isolation failures. Using sandboxes with proper afterEach cleanup is the recommended approach for most test suites. Always verify mock expectations, check spy call existence before accessing call properties, and ensure fake timers are restored to prevent cascading test failures.

Key Takeaways:

  1. Always restore: stubs, spies, mocks, and fake timers
  2. Use sandboxes for simplified cleanup management
  3. Check spy.called before accessing spy.firstCall
  4. Always call mock.verify() to enforce expectations
  5. Avoid double-stubbing without restore
  6. Be cautious with async test cleanup timing
  7. Prefer stubbing individual methods over entire objects
Need a different package?
Request a profile