Article summary
Lately, I’ve been using Sinon.JS for mocking in my unit tests. By using mocks for dependencies inside functions, I can write unit tests that are resilient to a changing codebase. Functions with mocks can also be developed without worrying about the chain of dependencies that could affect the logic inside the functions.
An Example
Let’s use this simple “greetUser” function to try out Sinon.JS.
import * as UserRepository from 'project/repos';
export async function greetUser(userId) {
const user = await UserRepository.findById(userId);
if (!user) {
throw new Error(`No user found for id ${id}`);
}
if (user.name) {
return `Hello, ${user.name}!`;
}
return 'Hello!';
}
This function is dependent on the implementation of UserRepository
. Based on the result of the findById
method, greetUser
will throw an error, create a message with the user’s name, or return a simple greeting.
Let’s start with a test that assumes we can find a user:
describe('greetUser', () => {
it('greets the user if one is found', async () => {
// We'll need to do some setup here soon...
const message = await greetUser(2);
expect(message).to.equal('Hello, Andy!');
});
});
In order for this test to pass, UserRepository.findById
needs to return a user. However, we don’t necessarily know (or even care) how UserRepository.findById
is implemented at this point. This unit test is concerned with the logic within the greetUser
function.
Instead of figuring out the implementation details of UserRepository.findById
, we can use Sinon.JS to create a stub:
describe('greetUser', () => {
it('greets the user if one is found', async () => {
sinon.stub(UserRepository, 'findById').returns({ name: 'Andy' });
const userId = 2;
const message = await greetUser(userId);
expect(message).to.equal('Hello, Andy!');
});
});
Stubs let us force a specific behavior on the UserRepository
, allowing us to exercise the different logical branches in the greetUser
function. With a stub, the development of the UserRepository
is decoupled from the development of this particular function. We can build and fully unit-test this function without writing a single line of code for the UserRepository</code.
Let’s force another behavior that’ll exercise a different logical branch of greetUser
. In this example, we can test what happens if the user doesn’t have a name, like this:
it('generates a generic message if name is not specified', async () => {
sinon.stub(UserRepository, 'findById').returns({ name: null });
const userId = 2;
const message = await greetUser(userId);
expect(message).to.equal('Hello!');
});
We can also test the case when no user is found:
it('throws an error when no user is found', async () => {
sinon.stub(UserRepository, 'findById').returns(null);
const userId = 2;
expect(await greetUser(userId)).to.be.rejected;
});
With these tests, we’ve exercised all the branches of our code without worrying about the implementation of UserRepository
.
A Few Considerations
Since this test is decoupled from the implementation of UserRepository
, we should add another test that does not mock out anything. This helps ensure that everything is wired together correctly.
Mocking prevents this suite of greetUser
tests from failing whenever UserRepository
is broken, but the test suite is vulnerable to false positives if there are no tests around UserRepository
and a system level test that uses both without mocks.
Mocks allow us to write unit tests that only test the internals of a particular function. As a codebase grows, these tests won’t have to change if the logic inside the function remains unchanged.
What other methods do you rely on for unit testing?
Sinon is great and i used it for years. For newer projects I have been using testdouble.js though: https://github.com/testdouble/testdouble.js/.
It is a bit more opinionated than Sinon which can at times be annoying, but it has a much nicer API IMO. Might be fun to try out on a new project at some point.
Thanks for sharing this Al. I can see what you mean by a nicer API when looking through the side-by-side comparison (http://blog.testdouble.com/posts/2016-03-13-testdouble-vs-sinon.html). I’ll definitely look into testdouble.js for my other projects.