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; });