Mocha Testing Cookbook Part 2: 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! 👩🍳
🔗 This article focuses on two types of test doubles: stubs
and spies
. If you are not sure which is the most appropriate for your needs, my Test doubles in Sinon article may prove helpful.
This is Part 2 of the Mocha Testing Cookbook series. Check out the other parts:
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 I will omit this statement in the 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 I will demonstrate using stubs and spies in real-life unit tests. Happy coding!