sinon
>=1.0.0postconditions22functions12last verified2026-06-23coverage score80%Postconditions: what we check
- stub · stub-must-restoreerrorWhensinon.stub(obj, 'method') is calledThrows
TypeError: Attempted to wrap [method] which is already wrappedRequired 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 unavailablevisibilityvisibleSources[1] - stub · stub-non-existent-propertyerrorWhensinon.stub is called on non-existent or non-function propertyThrows
TypeError: 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 unavailablevisibilityvisibleSources[2] - stub · stub-returns-arg-invalid-indexerrorWhenstub.returnsArg(index) is called with index >= argument countThrows
TypeError: index unavailableRequired 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 unavailablevisibilityvisibleSources[3] - stub · stub-calls-arg-not-functionerrorWhenstub.callsArg(index) is called with non-function argumentThrows
TypeError: index missing or not a functionRequired handlingMUST verify argument at index is a function before calling stub.callsArg(index).costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - stub · stub-yields-no-callbackerrorWhenstub.yields() is called but stub was never called with a function argumentThrows
Error: stub was never called with a function argumentRequired handlingMUST ensure stub is called with at least one function argument before using yields().costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[3] - spy · spy-first-call-null-accesserrorWhenspy.firstCall is accessed when spy was never calledThrows
TypeError: Cannot read property 'args' of nullRequired 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 unavailablevisibilityvisibleSources[4] - spy · spy-must-restoreerrorWhensinon.spy(obj, 'method') is calledThrows
TypeError: Attempted to wrap [method] which is already wrappedRequired 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 unavailablevisibilityvisibleSources[5] - mock · mock-verify-not-callederrorWhenmock.expects() is called but mock.verify() is never calledThrows
Silent failure - expectations not enforcedRequired 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 unavailablevisibilityvisibleSources[6] - mock · mock-verify-failsinfoWhenmock.verify() is called and expectations are not metThrows
ExpectationErrorRequired handlingThis is expected behavior when expectations fail. Ensure expectations match actual usage.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[6] - useFakeTimers · fake-timers-must-restoreerrorWhensinon.useFakeTimers() is calledThrows
Test hangs, setTimeout/setInterval never fire in subsequent testsRequired 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 unavailablevisibilityvisibleSources[7] - createSandbox · sandbox-must-restoreerrorWhensinon.createSandbox() is calledThrows
Test pollution, 'already wrapped' errors in subsequent testsRequired 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 unavailablevisibilityvisibleSources[8] - createStubInstance · create-stub-instance-double-useerrorWhensinon.createStubInstance is called twice on same constructor without cleanupThrows
TypeError: Attempted to wrap [method] which is already wrappedRequired handlingMUST restore or use different sandbox between createStubInstance calls. Pattern: Use sandbox.createStubInstance() and restore sandbox between uses.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisibleSources[9] - fake · fake-non-function-arg-throwserrorWhensinon.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.Throws
TypeError: Expected f argument to be a FunctionRequired 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 - fake · fake-yields-missing-callbackerrorWhenA 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.Throws
TypeError: Expected last argument to be a functionRequired 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 - replace · replace-non-existent-propertyerrorWhensandbox.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.Throws
TypeError: 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 - replace · replace-already-replaced-throwserrorWhensandbox.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.Throws
TypeError: Attempted to replace '<property>' which is already replacedRequired 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 - replace · replace-accessor-property-throwserrorWhensandbox.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.Throws
Error: Use sandbox.replaceGetter for replacing gettersRequired 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 - replace · replace-type-mismatch-throwserrorWhensandbox.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.Throws
TypeError: 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 - define · define-existing-property-throwserrorWhensandbox.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).Throws
TypeError: 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 - verify · verify-unmet-expectation-throwserrorWhensandbox.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.Throws
ExpectationError: expected <method> to be called with arguments matching ... but was called <n> timesRequired 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 - verifyAndRestore · verify-and-restore-expectation-throwserrorWhensandbox.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.Throws
ExpectationError: expected <method> to be called with arguments matching ... but was called <n> timesRequired 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 - restoreObject · restore-object-falsy-input-throwserrorWhensinon.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.Throws
Error: 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
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
- [11]github.com/sinonjs/sinon/blobsinonjs/sinon · fake.js
- [12]github.com/sinonjs/sinon/blobsinonjs/sinon · sandbox.js
- [13]github.com/sinonjs/sinon/blobsinonjs/sinon · sandbox.js
- [15]github.com/sinonjs/sinon/blobsinonjs/sinon · restore-object.js
- [1]github.com/sinonjs/sinon/issuessinonjs/sinon issue #1673
- [2]github.com/sinonjs/sinon/issuessinonjs/sinon issue #1762
- [4]github.com/sinonjs/sinon/issuessinonjs/sinon issue #1936
- [5]github.com/sinonjs/sinon/issuessinonjs/sinon issue #1775
- [9]github.com/sinonjs/sinon/issuessinonjs/sinon issue #852
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.createStubInstancecannot 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.firstCallreturns null when usingwithArgs(), even thoughgetCall(0)works - Issue #1487: Incorrect returnValue when using
withArgs()withfirstCallorlastCall - 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 valuestub.returnsArg(index)- Returns argument at index (throws TypeError if unavailable in v6.1.2+)stub.returnsThis()- Returnsthiscontext (for fluent APIs)
Promises:
stub.resolves(value)- Returns Promise resolving to valuestub.resolvesArg(index)- Returns Promise resolving to argument (throws TypeError if unavailable)stub.rejects()- Returns rejected Promisestub.rejects("TypeError")- Returns Promise rejected with typed exception
Exceptions:
stub.throws()- Throws generic Errorstub.throws("name"[, "message"])- Throws named exceptionstub.throws(obj)- Throws provided exception objectstub.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 argumentsstub.yields([arg1, arg2])- Calls first function argumentstub.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 historystub.resetBehavior()- Resets to default behavior onlystub.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
- Anonymous Spy:
const spy = sinon.spy();
- Function Wrapper:
const spy = sinon.spy(myFunc);
- Method Spy:
const spy = sinon.spy(object, "method");
- Property Accessor Spy:
const spy = sinon.spy(object, "property", ["get", "set"]);
Call Information Properties
Call Counters:
spy.callCount- Total number of callsspy.called- Boolean: at least one callspy.notCalled- Boolean: no callsspy.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 objectspy.thirdCall- Third call objectspy.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 callspy.thisValues[n]- Context objects for each callspy.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 argumentsspy.calledWithExactly(...)- Exact argument matchspy.calledOnceWith(...)- Exactly one call with argumentsspy.alwaysCalledWith(...)- All calls match argumentsspy.calledWithMatch(...)- Matcher-based comparison
Context Verification:
spy.calledOn(obj)- Verifiesthiscontextspy.alwaysCalledOn(obj)- All calls used target contextspy.calledWithNew()- Detects constructor invocation
Call Ordering:
spy.calledBefore(anotherSpy)- Temporal orderingspy.calledAfter(anotherSpy)- Reverse orderingspy.calledImmediatelyBefore(anotherSpy)- Sequential callsspy.calledImmediatelyAfter(anotherSpy)- Immediate successor
Exception/Return Tracking:
spy.threw()- Returns true if spy threw exceptionspy.threw("TypeError")- Specific exception type checkspy.alwaysThrew()- All invocations threwspy.returned(obj)- Verifies returned valuespy.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 countexpectation.atMost(number)- Maximum call countexpectation.exactly(number)- Exact call countexpectation.never()- Method should never be calledexpectation.once()- Exactly one callexpectation.twice()- Exactly two callsexpectation.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 specificthiscontext
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 completelysandbox.reset()- Resets internal state of all fakessandbox.resetHistory()- Clears call history onlysandbox.resetBehavior()- Resets stub behaviorssandbox.verify()- Validates mock expectationssandbox.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" formatsclock.jump(time)- Skips forward, fires callbacks at most once (simulates sleep/resume)clock.next()- Advances to the first scheduled timerclock.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 executionawait clock.nextAsync()- Advances asynchronouslyawait 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)
-
Missing restore() on stubs/spies/mocks
- Causes test pollution
- Blocks subsequent tests
- TypeErrors: "already wrapped"
-
Missing clock.restore() on fake timers
- Breaks all subsequent tests using timers
- Causes test hangs
- Tests may never complete
-
Missing mock.verify()
- Expectations silently not enforced
- False confidence in test coverage
- Tests pass when they should fail
-
Accessing spy calls without existence checks
- TypeError: Cannot read property 'args' of null
- Runtime test failures
- Brittle tests
WARNING (Should Fix)
-
Not using sandboxes for complex test suites
- Increased risk of cleanup errors
- More maintenance overhead
- Harder to track all fakes
-
Over-using mocks instead of stubs
- Brittle tests tied to implementation
- Harder to refactor
- False failures on benign changes
-
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:
- Sinon.JS Homepage
- API Documentation (v20.0.0)
- Stubs Guide
- Spies Guide
- Mocks Guide
- Sandbox Guide
- Fake Timers Guide
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:
- Always restore: stubs, spies, mocks, and fake timers
- Use sandboxes for simplified cleanup management
- Check spy.called before accessing spy.firstCall
- Always call mock.verify() to enforce expectations
- Avoid double-stubbing without restore
- Be cautious with async test cleanup timing
- Prefer stubbing individual methods over entire objects