Sinon Mocks
Spies โ Observe Function Calls
const sinon = require('sinon');
// Spy on an existing function
const spy = sinon.spy(user, 'save');
user.save({ name: 'Alice' });
// Check calls
spy.callCount; // 1
spy.calledOnce; // true
spy.calledWith({ name: 'Alice' }); // true
spy.firstCall.args; // [{ name: 'Alice' }]
spy.firstCall.returnValue; // whatever save() returned
spy.threw(); // true if it threw
// Standalone spy (replaces nothing)
const logSpy = sinon.spy();
eventEmitter.on('event', logSpy);
eventEmitter.emit('event', 'data');
assert(logSpy.calledWith('data'));
// Always restore spies
afterEach(() => {
spy.restore();
});
Stubs โ Replace Implementations
const sinon = require('sinon');
// Stub a method
const stub = sinon.stub(db, 'query');
stub.returns([{ id: 1, name: 'Alice' }]);
stub.resolves({ id: 1 }); // async
stub.rejects(new Error('DB down')); // async reject
// Conditional behavior
stub.withArgs('SELECT *').returns([]);
stub.withArgs('SELECT id').returns([{ id: 1 }]);
// onCall behavior
stub.onFirstCall().returns(null);
stub.onSecondCall().returns({ id: 1 });
stub.returns({ id: 99 }); // default fallback
// Stub properties
const obj = { value: 42 };
sinon.stub(obj, 'value').value(100); // property stub
// Restore
stub.restore();
// Callthrough to original
stub.callThrough();
// Call a callback argument
stub.callsFake((query, cb) => cb(null, [{ id: 1 }]));
Mocks โ Pre-configured Expectations
const sinon = require('sinon');
// Mocks verify expectations automatically
const mock = sinon.mock(db);
// Set up expectations
mock.expects('query')
.once()
.withArgs('SELECT * FROM users')
.returns([{ id: 1 }]);
mock.expects('close').once();
// Exercise code under test
const users = db.query('SELECT * FROM users');
db.close();
// Verify all expectations met (throws if not)
mock.verify();
mock.restore();
// Mocks enforce behavior โ use stubs for simpler cases
Fake Timers
const sinon = require('sinon');
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers(new Date('2024-01-01'));
});
afterEach(() => {
clock.restore();
});
it('triggers callback after delay', () => {
const callback = sinon.spy();
setTimeout(callback, 1000);
clock.tick(999);
assert(callback.notCalled);
clock.tick(1);
assert(callback.calledOnce);
});
it('polls at intervals', () => {
const poll = sinon.spy();
setInterval(poll, 500);
clock.tick(2000);
assert.equal(poll.callCount, 4);
});
// Mock Date.now()
assert.equal(Date.now(), new Date('2024-01-01').getTime());
// Only fake specific globals
clock = sinon.useFakeTimers({
toFake: ['setTimeout', 'setInterval', 'Date'],
});
Sandbox โ Grouped Cleanup
const sinon = require('sinon');
describe('UserService', () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
// Restores ALL stubs/spies/mocks created via sandbox
sandbox.restore();
});
it('fetches users', async () => {
const stub = sandbox.stub(db, 'query').resolves([{ id: 1 }]);
sandbox.spy(logger, 'info');
const users = await service.getAll();
assert(stub.calledOnce);
assert(logger.info.calledWith('Fetched 1 users'));
assert.deepEqual(users, [{ id: 1 }]);
});
});
sinon-chai Integration
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
chai.use(sinonChai);
const { expect } = chai;
it('stub was called correctly', () => {
const stub = sinon.stub(api, 'post').resolves({ ok: true });
await service.createUser({ name: 'Alice' });
expect(stub).to.have.been.calledOnce;
expect(stub).to.have.been.calledWith('/users', { name: 'Alice' });
expect(stub).to.have.been.calledWithMatch(sinon.match.has('name'));
expect(stub).to.not.have.been.calledTwice;
});