Migrating from Stubs to Fakes
Fakes are the modern, preferred alternative to stubs for most use cases. They provide simpler, immutable behavior while maintaining the same spy API. This guide shows how to migrate your code from stubs to fakes.
Why Migrate to Fakes?
- Immutable - Fakes can't change behavior after creation, preventing bugs
- Simpler - No need to chain multiple behavior methods
- Same spy API - All spy assertions work on fakes
- Clearer intent - Behavior is defined at creation time
When to Keep Using Stubs
Stubs have unique features that fakes don't provide:
- Call-specific behavior -
onCall(),onFirstCall(),onSecondCall(),onThirdCall() - Argument-based behavior -
withArgs()for different returns per arguments - Property stubbing -
.get(),.set(),.value()for property accessors - Mutable behavior - When you need to change behavior mid-test (usually a code smell)
For these cases, continue using stubs. For everything else, prefer fakes.
Migration Patterns
Simple Return Values
Before (Stub):
const stub = sinon.stub().returns(42);
const result = stub();
assert.equal(result, 42);After (Fake):
const fake = sinon.fake.returns(42);
const result = fake();
assert.equal(result, 42);Change: Replace sinon.stub().returns() with sinon.fake.returns()
Returning Arguments
Before (Stub):
const stub = sinon.stub().returnsArg(0);
stub("hello"); // returns 'hello'
stub("world"); // returns 'world'After (Fake):
// Fakes don't have returnsArg, use callsFake
const fake = sinon.fake((arg) => arg);
fake("hello"); // returns 'hello'
fake("world"); // returns 'world'Change: Use sinon.fake(func) with a function that returns the argument
Throwing Errors
Before (Stub):
const stub = sinon.stub().throws(new Error("Failed"));
assert.throws(() => stub());After (Fake):
const fake = sinon.fake.throws(new Error("Failed"));
assert.throws(() => fake());Change: Replace sinon.stub().throws() with sinon.fake.throws()
Promise Resolution
Before (Stub):
const stub = sinon.stub().resolves("success");
const result = await stub();
assert.equal(result, "success");After (Fake):
const fake = sinon.fake.resolves("success");
const result = await fake();
assert.equal(result, "success");Change: Replace sinon.stub().resolves() with sinon.fake.resolves()
Promise Rejection
Before (Stub):
const stub = sinon.stub().rejects(new Error("Failed"));
try {
await stub();
} catch (e) {
assert.equal(e.message, "Failed");
}After (Fake):
const fake = sinon.fake.rejects(new Error("Failed"));
try {
await fake();
} catch (e) {
assert.equal(e.message, "Failed");
}Change: Replace sinon.stub().rejects() with sinon.fake.rejects()
Custom Function Logic
Before (Stub):
const stub = sinon.stub().callsFake(function (x) {
return x * 2;
});After (Fake):
const fake = sinon.fake(function (x) {
return x * 2;
});Change: Pass function directly to sinon.fake() instead of chaining .callsFake()
Callback Invocation (Sync)
Before (Stub):
const stub = sinon.stub().yields("error", "data");
stub((err, data) => {
assert.equal(err, "error");
assert.equal(data, "data");
});After (Fake):
const fake = sinon.fake.yields("error", "data");
fake((err, data) => {
assert.equal(err, "error");
assert.equal(data, "data");
});Change: Replace sinon.stub().yields() with sinon.fake.yields()
Callback Invocation (Async)
Before (Stub):
const stub = sinon.stub().yieldsAsync("error", "data");
// callback invoked asynchronouslyAfter (Fake):
const fake = sinon.fake.yieldsAsync("error", "data");
// callback invoked asynchronouslyChange: Replace sinon.stub().yieldsAsync() with sinon.fake.yieldsAsync()
Replacing Object Methods
Before (Stub):
const obj = {
method() {
return "original";
}
};
sinon.stub(obj, "method").returns("stubbed");
obj.method(); // 'stubbed'After (Fake):
const obj = {
method() {
return "original";
}
};
const fake = sinon.fake.returns("stubbed");
sinon.replace(obj, "method", fake);
obj.method(); // 'stubbed'Change: Create fake separately, then use sinon.replace() to plug it in
Patterns That Require Stubs
Call-Specific Behavior (Keep Stubs)
Some scenarios require call-specific behavior which fakes don't support:
// This pattern requires stubs
const stub = sinon
.stub()
.onFirstCall()
.returns(1)
.onSecondCall()
.returns(2)
.returns(3);
stub(); // 1
stub(); // 2
stub(); // 3
stub(); // 3No fake equivalent - Continue using stubs for this pattern.
Alternative: If you control the calls, create separate fakes:
const fake1 = sinon.fake.returns(1);
const fake2 = sinon.fake.returns(2);
const fake3 = sinon.fake.returns(3);
// Use appropriate fake at each call siteArgument-Based Behavior (Keep Stubs)
Returning different values based on arguments requires stubs:
// This pattern requires stubs
const stub = sinon.stub();
stub.withArgs("apple").returns("fruit");
stub.withArgs("carrot").returns("vegetable");
stub.returns("unknown");
stub("apple"); // 'fruit'
stub("carrot"); // 'vegetable'
stub("pizza"); // 'unknown'No fake equivalent - Continue using stubs for this pattern.
Alternative: Use a fake with conditional logic:
const fake = sinon.fake((food) => {
if (food === "apple") return "fruit";
if (food === "carrot") return "vegetable";
return "unknown";
});Property Stubbing (Keep Stubs)
Stubbing property getters/setters requires stubs:
// This pattern requires stubs
const obj = {
get name() {
return "Alice";
}
};
sinon.stub(obj, "name").get(() => "Bob");
obj.name; // 'Bob'No fake equivalent - Continue using stubs for property stubbing.
Key Differences
Immutability
Stubs (Mutable):
const stub = sinon.stub();
stub.returns(1); // Changes behavior
stub(); // 1
stub.returns(2); // Changes behavior again
stub(); // 2Fakes (Immutable):
const fake1 = sinon.fake.returns(1);
const fake2 = sinon.fake.returns(2);
fake1(); // 1
fake2(); // 2
// fake1 still returns 1, fake2 returns 2Behavior Chaining
Stubs (Chained):
const stub = sinon.stub().onFirstCall().returns(1).onSecondCall().returns(2);Fakes (No Chaining):
// Behavior defined at creation
const fake = sinon.fake.returns(1);
// No way to change behaviorCreation vs Plugging In
Stubs (Combined):
// Stub creation and plugging in are combined
sinon.stub(obj, "method").returns(42);Fakes (Separated):
// Creation and plugging in are separate
const fake = sinon.fake.returns(42);
sinon.replace(obj, "method", fake);Migration Checklist
- [ ] Identify all
sinon.stub()usage - [ ] Check for call-specific behavior (
onCall,onFirstCall, etc.) - keep as stub - [ ] Check for argument-based behavior (
withArgs) - keep as stub - [ ] Check for property stubbing (
.get(),.set(),.value()) - keep as stub - [ ] Replace simple stubs with appropriate fake factories:
- [ ]
stub().returns()→fake.returns() - [ ]
stub().throws()→fake.throws() - [ ]
stub().resolves()→fake.resolves() - [ ]
stub().rejects()→fake.rejects() - [ ]
stub().yields()→fake.yields() - [ ]
stub().yieldsAsync()→fake.yieldsAsync() - [ ]
stub().callsFake()→fake(func)
- [ ]
- [ ] Update object method stubbing to use
sinon.replace() - [ ] Verify all tests still pass
Decision Tree
Do you need call-specific behavior (onCall, onFirstCall)?
├─ Yes → Keep using stub
└─ No → Do you need argument-based behavior (withArgs)?
├─ Yes → Keep using stub
└─ No → Do you need property stubbing (.get, .set, .value)?
├─ Yes → Keep using stub
└─ No → Migrate to fake! ✨