Mocha Testing Cookbook - Pt. 2

Creating stubs and spies

Dependencies are an integral part of most codebases. They come in different forms:

  • internal dependencies: our own modules (functions and classes), used by other parts of our code
  • external dependencies: modules exposed by libraries, frameworks etc
  • physical dependencies: database, network connection etc

In unit testing, we want to test modules (units) "in a vacuum" in order to quickly and accurately identify the root causes of bugs. To do so, we have to replace dependencies with test doubles.

In this article we will leverage the power of Sinon.js to create test doubles that we can then use to replace/observe dependencies in our unit tests. Enjoy! ๐Ÿ‘ฉโ€๐Ÿณ

๐Ÿ”— We focus on two types of test doubles: stubs and spies. If you are not sure which is the most appropriate for your needs, our Test doubles in Sinon article may prove helpful.

Sinon logo

This is Part 2 of 3 articles on Mocha testing recipes, exploring test double creation.

Check out the first part for fundamental test recipes:

  • Array
  • Object
  • Errors
  • Async

Prerequisites

Sinon must be installed in your project. Using npm:

npm install -D sinon

Using yarn:

yarn install -D sinon

Also, sinon must be imported in your code before using it:

import sinon from 'sinon';

For the sake of brevity we will omit this statement in our examples.

Creating stubs

Sinon exposes a powerful and versatile Stub API which can match the level of detail we need in each case. In the following sections, we will examine techniques for creating function and class instance stubs.

Function stubs

Dummy implementation

The simplest form of stubbing is replacing a function with a dummy implementation which does nothing:

const stub = sinon.stub();

This is useful when the return value and/or behaviour of the function is not relevant to the specific test case. Examples:

  • We want to suppress a behaviour that is not desired in our tests, e.g. logging
  • The behaviour of the stubbed method is asserted in another test case

Single return value

In reality, we usually need to specify an appropriate return value for our stub so that it can be consumed by the code under test. Doing so it pretty straightforward:

// Sync function - use `returns`
const getBands = sinon.stub().returns(['Gojira', 'Mastodon']);

// Async function - use `resolves`
const fetchBands = sinon.stub().resolves(['Gojira', 'Mastodon']);

โš ๏ธDon't forget to use resolves (and not returns) for a stubbed async function, in order to properly replicate its asynchronous behaviour.

Conditional return values

This may be the most common scenario in practice: we want to simulate a function's behaviour by returning values based on the provided input. We can do so fluently using Sinon's withArgs API:

// sync function - use `returns`
const getBands = sinon.stub();
getBands
.withArgs({ genre: 'thrash metal' })
.returns(['Metallica', 'Slayer']);

// async function - use `resolves`
const fetchBands = sinon.stub();
fetchBands
.withArgs({ genre: 'thrash metal' })
.resolves(['Metallica', 'Slayer']);

// we can chain multiple conditions:
const getAlbums = sinon.stub();
getAlbums
.withArgs({
artist: 'Metallica',
date: { year: 1986, month: 3 },
})
.returns(['Master of Puppets'])
.withArgs({
artist: 'Megadeth',
date: { year: 1990, month: 9 },
})
.returns(['Rust in Peace']);

๐Ÿ‘‰Deep equality is used to match the specified arguments. Any missing/extra fields will fail the check:

getAlbums({
date: { month: 3, year: 1986 },
artist: 'Metallica',
}); // [ 'Master of Puppets' ]

getAlbums({
date: { month: 3 },
artist: 'Metallica',
}); // undefined

getAlbums({
artist: 'Metallica',
date: { year: 1986, month: 3 },
label: 'Elektra Records',
}); // undefined

๐Ÿ‘‰If no call argument matches a withArgs argument, undefined is returned. We can place a returns/resolves method first in the chain to specify a default return value. This often leads to a more accurate simulation of the stubbed method:

const getLabels = sinon.stub();
getLabels
.returns([]) // default value
.withArgs({ country: 'UK' })
.returns(['Elektra Records']);

getLabels({ country: 'UK' }); // [ 'Elektra Records' ];
getLabels({ country: 'Gondor' }); // []

โš ๏ธ It is important to separate the creation statement from chaining withArgs methods. In case we don't, only the last returns/resolves method will be used:

// Wrong
const boolToIntWrong = sinon
.stub()
.withArgs(true)
.returns(1)
.withArgs(false)
.returns(0);

boolToIntWrong(false); // 0
boolToIntWrong(true); // 0 again...

// Correct
const boolToIntCorrect = sinon.stub();
boolToIntCorrect.withArgs(true).returns(1).withArgs(false).returns(0);

boolToIntCorrect(false); // 0
boolToIntCorrect(true); // 1

Specific implementation

In nothing of the above is suitable in our use case, we can provide a specific implementation using callsFake(). In some cases it may also make our stub simpler/cleaner:

const albums = [
{ title: 'Master of Puppets', year: 1986 },
{ title: 'Rust in Peace', year: 1990 },
];

const getAlbums = sinon.stub().callsFake(({ title, year }) => {
let results = albums;
if (title) {
results = results.filter(
({ title: currentTitle }) => currentTitle === title,
);
}
if (year) {
results = results.filter(
({ year: currentYear }) => currentYear === year,
);
}

return results;
});

getAlbums({ year: 1986 });
// [ { title: 'Master of Puppets', year: 1986 } ]

getAlbums({ title: 'Rust in Peace' });
// [ { title: 'Rust in Peace', year: 1990 } ]

getAlbums({ year: 1986, title: 'Rust in Peace' });
// []

Argument matchers

Sinon supports a flexible matchers API. With it we can match a more general aspect of a function's argument (e.g. its type), rather than its specific value. We may follow this approach because:

  • We want to break our tests in individual cases per parameter. This can effectively reduce the required test case combinations, especially for functions with more than 2 parameters.
  • Similarly to the above, our parameters have a polymorphic behaviour (can be of multiple types/formats etc)
const getDbRecords = (table, conditions, fields) => {
// Fetch database records from `table`
// If `conditions` are provided, use them to filter the results
// If `fields` are provided, include only the specified fields
};

const database = {
album: [
{ artist: 'Celtic Frost', title: 'Into the Pandemonium' },
{ artist: 'Iced Earth', title: 'The Dark Saga' },
],
};

const getDbRecordsStub = sinon.stub();
getDbRecordsStub
.withArgs('album')
.returns(database.album)
.withArgs(sinon.match.any, { artist: 'Celtic Frost' })
.returns([
{ artist: 'Celtic Frost', title: 'Into the Pandemonium' },
])
.withArgs(sinon.match.any, sinon.match.any, 'title')
.returns(['Into the Pandemonium']);

console.log(getDbRecordsStub('album'));
// [ { artist: 'Celtic Frost', title: 'Into the Pandemonium' },
// { artist: 'Iced Earth', title: 'The Dark Saga' } ]

console.log(getDbRecordsStub('album', { artist: 'Celtic Frost' }));
// [ { artist: 'Celtic Frost', title: 'Into the Pandemonium' } ]

console.log(
getDbRecordsStub('album', { artist: 'Celtic Frost' }, 'title'),
);
// [ 'Into the Pandemonium' ]

โš ๏ธ When multiple withArgs methods are chained, the last matching condition will be used. This is true even if an earlier matcher is more "specific":

const stubMethod = sinon.stub();
stubMethod
.withArgs(false)
.returns('false')
.withArgs(sinon.match.falsy)
.returns(0)
.withArgs(sinon.match.truthy)
.returns(1);

stubMethod(false); // 0, not 'false'

For this reason, it is a good practice to order our matchers from the most generic to the most specific.

Class instance stubs

Creating a stub that is an instance of a specific class is pretty easy using Sinon's createStubInstance API. All methods of the specified class will be automatically replaced by empty functions:

class AlbumApi {
constructor(database) {
this.database = database;
}

findOne(id) {
return this.database.getById('album', id);
}
}

const albums = [
{ id: 1, title: 'Revolted Masses' },
{ id: 2, title: 'Us or Them' },
{ id: 3, title: 'Rise' },
];

let api;

api = sinon.createStubInstance(AlbumApi);
api.findOne(); // undefined

api = sinon.createStubInstance(AlbumApi, {
// Provide a specific return value
findOne: { id: 1, title: 'Revolted Masses' },
});
api.findOne(1); // { id: 1, title: 'Revolted Masses' }

api = sinon.createStubInstance(AlbumApi, {
// Provide a custom implementation using sinon.stub()
findOne: sinon
.stub()
.callsFake(
id =>
albums.find(({ id: currentId }) => id === currentId) || null,
),
});
api.findOne(2); // { id: 2, title: 'Us or Them' }

api = sinon.createStubInstance(AlbumApi, {
// Will throw error: "Cannot stub getAllIds. Property does not exist!"
getAllIds: [1, 2, 3],
});

As we can see in the above example, we can override specific methods of the class instance by passing an object as the second argument of createStubInstance. Its keys are the overridden method names, while its values must be either a specific value that the method will return, or a Sinon function stub. Any of the stub creation techniques that we demonstrated previously can be used in the second case.

โš ๏ธ The following will not work:

const api = sinon.createStubInstance(AlbumApi, {
// This will return the specified function instead of calling it!
// Use sinon.stub() if you want to provide a custom implementation
getById: id =>
albums.find(({ id: currentId }) => id === currentId) || null,
});

Creating spies

Stubbing the return value/behaviour of a dependency is a great way to set specific expectations for its correct usage. Another powerful technique to structure our test logic is using call assertions via spies.

In simple words, spies are test doubles that allow us to track how many times a function was called, as well as its exact arguments. Some use cases where we may want to do that:

  • We want to be sure that a remote API was called correctly. For example, we are updating an entity via a CRUD operation on a remote server
  • Our function may invoke different APIs based on its input. We want to write a test case that tests whether this is done correctly

The syntax for creating a spy is identical to the one for creating a stub, the only difference being that a spy will not replace the original functionality, but rather observe its call history.

const spy = sinon.spy();

const invokeCallback = callback => callback();

console.log(spy.callCount); // 0
invokeCallback(spy);
console.log(spy.callCount); // 1
invokeCallback(spy);
console.log(spy.callCount); // 2
import sinon from 'sinon';

const albums = [
{ id: 1, title: 'Revolted Masses' },
{ id: 2, title: 'Us or Them' },
{ id: 3, title: 'Rise' },
];

class AlbumApi {
findOne(id) {
return albums[id] || null;
}
}

const albumApi = new AlbumApi();
const spy = sinon.spy(albumApi, 'findOne');

console.log(spy.calledWith(1)); // false

albumApi.findOne(1);
console.log(spy.calledWith(1)); // true
console.log(spy.calledWith(2)); // false

albumApi.findOne(2);
console.log(spy.calledWith(1)); // true
console.log(spy.calledWith(2)); // true

albumApi.findOne(1);
console.log(spy.calledWith(1)); // true
console.log(spy.calledOnceWith(1)); // false (has been called twice)

Epilogue

I hope you enjoyed this article! In the next and last part of the series we will demonstrate using stubs and spies in real-life unit tests. Happy coding!