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

Sinon logo

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