Mocha Testing Cookbook Part 3: Using test doubles
Replacing dependencies with test doubles (stubs, spies etc.) is fundamental in writing robust unit tests. At the same time, it may feel a bit complicated in practice, with similar cases requiring different approaches.
In this article, I will demonstrate using Sinon.js to replace dependencies with test doubles in real-life scenarios ("recipes"). Enjoy! 👩🍳
🔗 Check out my Test doubles in Sinon article for a refresher on the differences between test double types
This is Part 3 of the Mocha Testing Cookbook series. Check out the other parts:
Recipes
Injected dependency
Congratulations - you followed time-tested software practices and now your test development life is much easier! ☯ Stubbing injected dependencies is easy-peasy:
// dependency.js
export const getRandom = () => Math.random();
// unit.js
/**
* @param {Function} randomGeneratorFn
*/
export const unit = randomGeneratorFn =>
Math.floor(randomGeneratorFn() * 100);
// unit.test.js
import { expect } from 'chai';
import { unit } from './unit';
describe('unit()', () => {
const randomGeneratorFn = () => 0.41234567;
it('returns an integer in [0, 100)', () => {
expect(unit(randomGeneratorFn)).to.equal(41);
});
});
We don't even have to use Sinon
here. In actual (non test) usage, getRandom()
or any other compatible function could be passed in to unit()
- it is an implementation detail.
Imported dependency
Stubbing imported dependencies may feel complicated, but have no fear - Coding Licks to the rescue! The syntax depends on which module system we use.
ES6 imports
// dependency.js
export const getRandom = () => Math.random();
// unit.js
import { getRandom } from './dependency';
export const unit = () => Math.floor(getRandom() * 100);
// unit.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import * as Dependency from './dependency';
import { unit } from './unit';
describe('unit()', () => {
before(() => {
sinon.stub(Dependency, 'getRandom').returns(0.41234567);
});
after(() => {
sinon.restore();
});
it('returns an integer in [0, 100)', () => {
expect(unit()).to.equal(41);
});
});
Two important points:
- We have to import the whole module including the stubbed dependency:
import * as Dependency from './dependency';
The following will not work:import { getRandom } from './dependency';
- Don't forget to restore stubbed dependencies in the end! If not, they will remain stubbed in any unit tests that follow. It's a good idea to do so in an
after
/afterEach
block, as in the example above.
CommonJS imports
Here we follow a different approach than with ES6 modules. We'll have to use proxyquire to rewire our stubbed imports. In the following example, we do so in a before
block:
// dependency.js
const getRandom = () => Math.random();
module.exports = { getRandom };
// unit.js
const { getRandom } = require('./dependency');
const unit = () => Math.floor(getRandom() * 100);
module.exports = { unit };
// unit.test.js
const { expect } = require('chai');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const Dependency = require('./dependency');
describe('unit()', () => {
let unit;
before(() => {
unit = proxyquire('./unit', {
getRandom: sinon
.stub(Dependency, 'getRandom')
.returns(0.41234567),
}).unit;
});
it('returns an integer in [0, 100)', () => {
expect(unit()).to.equal(41);
});
});
Class instance method
// dependency.js
export class RandomGenerator {
get() {
return Math.random();
}
}
// unit.js
export const unit = generator => Math.floor(generator.get() * 100);
This is a subcategory of Injected Dependency. We have two options here:
Option 1: Use an object
// unit.test.js
import { expect } from 'chai';
import { unit } from './unit';
describe('unit()', () => {
const generatorStub = { get: () => 0.41234567 };
it('returns an integer in [0, 100)', () => {
expect(unit(generatorStub)).to.equal(41);
});
});
✔️ Simple as!
Option 2: Use sinon.createStubInstance
// unit.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import { RandomGenerator } from './dependency';
import { unit } from './unit';
describe('unit()', () => {
const generatorStub = sinon.createStubInstance(RandomGenerator, {
get: 0.41234567,
// We can also provide a custom implementation:
// get: sinon.stub().callsFake(() => 0.41234567),
});
it('returns an integer in [0, 100)', () => {
expect(unit(generatorStub)).to.equal(41);
});
});
✔️ Throws an error if a method doesn't exist in the class:
const generatorStub = sinon.createStubInstance(RandomGenerator, {
wrongMethod: 0.41234567,
});
// Will throw "Error: Cannot stub wrongMethod. Property does not exist!"
✔️ Replaces all methods with dummy implementations by default - useful for quick stubbing.
Class prototype method
Occasionally we want to stub specific methods in the class prototype itself, rather than in an instance of it. This is required when a class instance is instantiated directly in the class under test, rather than injected in it.
// dependency.js
export class RandomGenerator {
get() {
return Math.random();
}
}
// unit.js
import { RandomGenerator } from './dependency';
export const unit = () =>
Math.floor(new RandomGenerator().get() * 100);
// unit.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import { RandomGenerator } from './dependency';
import { unit } from './unit';
describe('unit()', () => {
before(() => {
sinon.stub(RandomGenerator.prototype, 'get').returns(0.41234567);
});
after(() => {
RandomGenerator.prototype.get.restore();
});
it('returns an integer in [0, 100)', () => {
expect(unit()).to.equal(41);
});
});
Static class method
// dependency.js
export class RandomGenerator {
static get() {
return Math.random();
}
}
// unit.js
import { RandomGenerator } from './dependency';
export const unit = () => Math.floor(RandomGenerator.get() * 100);
// unit.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import * as Dependency from './dependency';
import { unit } from './unit';
describe('unit()', () => {
before(() => {
sinon.stub(Dependency.RandomGenerator, 'get').returns(0.41234567);
});
after(() => {
sinon.restore();
});
it('returns an integer in [0, 100)', () => {
expect(unit()).to.equal(41);
});
});
Datetime
We can use Sinon's Fake Timers:
// unit.js
const twoDigitFormat = number =>
number.toString().padStart(2, '0').substring(0, 2);
export const getDateString = () => {
const date = new Date();
const year = date.getFullYear();
const month = twoDigitFormat(date.getMonth() + 1);
const day = twoDigitFormat(date.getDate());
return `The date today is ${year}-${month}-${day}`;
};
// unit.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import { getDateString } from './unit';
const TIMESTAMPS = {
'2020-01-01': 1577836800000,
'2020-01-10': 1578614400000,
'2020-10-01': 1601510400000,
};
const fakeNow = timestamp => sinon.useFakeTimers({ now: timestamp });
describe('getDateString()', () => {
it('month < 10 and day < 10', () => {
fakeNow(TIMESTAMPS['2020-01-01']);
expect(getDateString()).to.equal(`The date today is 2020-01-01`);
});
it('month >= 10', () => {
fakeNow(TIMESTAMPS['2020-10-01']);
expect(getDateString()).to.equal(`The date today is 2020-10-01`);
});
it('day >= 10', () => {
fakeNow(TIMESTAMPS['2020-01-10']);
expect(getDateString()).to.equal(`The date today is 2020-01-10`);
});
});