docs/concepts/mocks/migrating-from-mocks.md
Mocks combine three concerns: behavior replacement, call observation, and assertions. Modern testing prefers separation of concerns using fakes with explicit assertions. This guide shows how to migrate from mocks to simpler alternatives.
Mocks are powerful but have drawbacks:
Fakes with explicit assertions are better:
Keep mocks when:
For most cases, use fakes.
Before (Mock):
const obj = {
method() {
return "original";
}
};
const mock = sinon.mock(obj);
mock.expects("method").once();
obj.method();
mock.verify();
After (Fake):
const obj = {
method() {
return "original";
}
};
const fake = sinon.fake.returns("result");
sinon.replace(obj, "method", fake);
obj.method();
assert.ok(fake.calledOnce);
Benefits:
Before (Mock):
mock.expects("greet").withArgs("Alice");
obj.greet("Alice");
mock.verify();
After (Fake):
const fake = sinon.fake();
sinon.replace(obj, "greet", fake);
obj.greet("Alice");
assert.ok(fake.calledWith("Alice"));
Benefits:
Before (Mock):
mock.expects("log").atLeast(2).atMost(5);
// ... actions ...
mock.verify();
After (Fake):
const fake = sinon.fake();
sinon.replace(logger, "log", fake);
// ... actions ...
assert.ok(fake.callCount >= 2, "called at least twice");
assert.ok(fake.callCount <= 5, "called at most 5 times");
Benefits:
Before (Mock - using stub behavior):
mock.expects("getData").once().returns({ id: 1, name: "Alice" });
const result = obj.getData();
mock.verify();
After (Fake):
const fake = sinon.fake.returns({ id: 1, name: "Alice" });
sinon.replace(obj, "getData", fake);
const result = obj.getData();
assert.deepEqual(result, { id: 1, name: "Alice" });
assert.ok(fake.calledOnce);
Benefits:
Before (Mock with stub behavior):
mock
.expects("fetch")
.onFirstCall()
.returns("first")
.onSecondCall()
.returns("second");
obj.fetch(); // 'first'
obj.fetch(); // 'second'
mock.verify();
After Option 1 (Keep using stub for this case):
const stub = sinon
.stub(obj, "fetch")
.onFirstCall()
.returns("first")
.onSecondCall()
.returns("second");
obj.fetch(); // 'first'
obj.fetch(); // 'second'
assert.ok(stub.calledTwice);
After Option 2 (Fake with custom logic):
const calls = ["first", "second"];
let callIndex = 0;
const fake = sinon.fake(() => calls[callIndex++]);
sinon.replace(obj, "fetch", fake);
obj.fetch(); // 'first'
obj.fetch(); // 'second'
assert.equal(fake.callCount, 2);
Note: For sequential behavior, stubs are often the best choice.
Before (Mock):
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2", method() {} };
const mock = sinon.mock(obj2);
mock.expects("method").on(obj1);
obj2.method.call(obj1);
mock.verify();
After (Fake):
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2", method() {} };
const fake = sinon.fake();
sinon.replace(obj2, "method", fake);
obj2.method.call(obj1);
assert.equal(fake.thisValues[0], obj1);
Benefits:
this valuesBefore (Mock):
mock.expects("dangerousMethod").never();
// ... actions ...
mock.verify();
After (Fake):
const fake = sinon.fake();
sinon.replace(obj, "dangerousMethod", fake);
// ... actions ...
assert.ok(fake.notCalled, "dangerous method should not be called");
Benefits:
Before (Mock):
const mock = sinon.mock(service);
mock.expects("load").once();
mock.expects("process").once();
mock.expects("save").once();
// ... actions ...
mock.verify();
After (Fake):
const loadFake = sinon.fake.returns("data");
const processFake = sinon.fake.returns("processed");
const saveFake = sinon.fake.resolves();
sinon.replace(service, "load", loadFake);
sinon.replace(service, "process", processFake);
sinon.replace(service, "save", saveFake);
// ... actions ...
assert.ok(loadFake.calledOnce, "load called once");
assert.ok(processFake.calledOnce, "process called once");
assert.ok(saveFake.calledOnce, "save called once");
Benefits:
sinon.assert.callOrder(loadFake, processFake, saveFake)Mocks make it easy to test implementation details. Avoid this:
Bad (Mock on implementation):
// Testing HOW the code works
const mock = sinon.mock(database);
mock.expects("query").withArgs("SELECT * FROM users");
mock.expects("query").withArgs("SELECT * FROM posts");
controller.getUserWithPosts(userId);
mock.verify();
Good (Fake on behavior):
// Testing WHAT the code does
const fake = sinon.fake.resolves({ user: userData, posts: postsData });
sinon.replace(database, "getUserWithPosts", fake);
const result = await controller.getUserWithPosts(userId);
assert.deepEqual(result.user, expectedUser);
assert.deepEqual(result.posts, expectedPosts);
assert.ok(fake.calledWith(userId));
Why this is better:
Find all sinon.mock() usage in your codebase:
grep -r "sinon.mock" test/
Simple mocks (one expectation, no fancy behavior):
Medium mocks (multiple expectations, simple behavior):
Complex mocks (many expectations, complex behavior):
For each mock:
After migration:
Is the expectation about behavior or implementation?
├─ Behavior → Use fake + explicit assertion
└─ Implementation → Do you really need to test this?
├─ Yes, it's critical → Keep mock (rare)
└─ No, it's an implementation detail → Refactor test to check behavior
sinon.mock() usage