What the Heck is Sinon
By: Brad
Hey Brad I want to write some unit tests, that is tests which don’t need me to stand up and seed a database just to test my codes logic, but how do I mock out calls to backing functions I don’t want to actually make during the test?
That is a good question, in my article What the Heck is Mocha I explained that mocha
is a testing framework for running and defining test. It is only the framework; it does not even include an assertions library, for that you need to pull in chai. If mocha
does not come packaged with an assertion library it definitely does not come with a mocking library. For that you need to use sinon
What is Sinon
Sinon is a set of stand alone APIs which let you wrap functions in spies or substitute them with stubs.
What is a Spy?
A spy is a decorator to a function your test might be calling. Lets say that you’re test is going to call function A which in turn will call function B; you want your test to prove that (a) function B was actually called and (b) what parameters were passed into it. For that you can use sinon
to wrap the function in a spy.
sinon.spy(userService, 'find');
The above will decorate the userService.find
function to add extra properties to it which allow us to know the number of times it is called.
userService.find.called // true if called at least once userService.find.calledOnce // true only if called exactly once
How it does this is by relaying on the reality that in JavaScript functions are assigned to properties/variables. The find
function is actually assigned to the property find
on the object userService
; what sinon
does is mutate the property userService.find
by re-assigning it to it’s own function which might look something like this:
userService.find = function (...args) { callCount += 1; return originalFindFunction(...args); } userService.find.called = () => (callCount > 0); userService.find.calledOnce = () => (callCount === 1);
This now allows us to spy on the userService.find
function to know exactly how many times it has been called.
sinon.spy()
not only tracks the number of times a function was called but also records what parameters were passed into the function when called.
userService.find .getCall(0) // get argument list for the first time it was called .args[0] // get the first parameter
To learn more about Spies see sinon’s documentation here
What is a Stub?
A spy is great when you want to verify that functions are only called the number of times you expect or with the parameters you expect them to be called with but what if you don’t actually want the function to be called? What if the actual function will write to a database and you don’t want your tests writing to the database? You can use sinon
to stub them out.
A stub is a fake function, it is invokable but does not actually do anything.
const callback = sinon.stub();
Here we simply generated a fake function which we can pass into our code as a callback. We do this to satisfy the code we’re testings contract without having to provide a real function to invoke.
But can’t we just pass in an empty function?
Well yes your right we could just create an empty function like so function callback(){}
and pass that in however sinon.stub()
also decorates the generated stub with a spy. So just like with sinon.spy()
we can check if the callback was called and with which parameters
const callback = sinon.stub(); ... callback.called // true if called at least once callback.calledOnce // true only if called exactly once ... callback.getCall(0) // get argument list for the first call .args[0] // get the first parameter
Not only can you generate stub functions but you can also substitute functions on objects (i.e. imported modules) with stubs to effectively mock out dependent modules.
sinon.stub(userService, 'find');
Like with sinon.spy()
what sinon
is doing here is mutating the userService.find
property, replacing it with its own function which could look something like this:
userService.find = function () { callCount += 1; return undefined; } userService.find.called = () => (callCount > 0); userService.find.calledOnce = () => (callCount === 1);
With the above if my test called userService.find
it won’t invoke the actual find function but my stub instead.
Well that is great and all but what if I needed that function to return something other then undefined
?
Not only does sinon.stub()
allow you to substitute a real function for a stub it also allows you to define the behaviour of said stub. It lets you define a number of different behaviours ranging from what to return, if it should invoke a callback or if it should throw an error; it even lets you define conditionals so it only throws given a certain input allowing the function to be called multiple times by your code and only throwing on the Nth call. sinon.stub()
allows you to define these behaviours via a fluent api.
sinon.stub(userService, 'find') .returns({ id: 12345, dob: 'April 5' });
To lean more about Stubs see sinon’s documentation here
How to use Sinon in your Mocha Test
Now that you know what sinon
is how do we use it? For an example lets say that we have a function under test which returns a users date of birth. It uses a backing module to look up the user but since we only care about our date of birth logic we want to stub out the service.
it('should return the users birthday', () => { sinon.stub(userService, 'find') .returns({ dob: 'April 5' }); expect(getBirthday(12345)) .to.equal('April 5'); });
This will pass, we’ve stubbed out the userService.find
function to always return the user object without fetching anything from the database.
But wait, there is a problem. Once we commit this test we find that a bunch of other tests are now failing, what happened?
The issue is that mocha
loads all your tests into one process and runs them all as a single JavaScript application and sinon.stub
is not scoped to a given test. This means that once your test is run all down stream tests will defacto be calling the same stubbed version of userService.find
. The failing tests were meant to call the real userService.find
but your test stubbed it out on them.
Remember
sinon.stub
is mutating theuserService.find
property
The good news it that sinon
has a solution for this, you just need to restore the stub once your done with it. This is easy enough to do because sinon
decorated the stub function with properties, one of which points to a restore function userService.find.restore()
.
it('should return the users birthday', () => { sinon.stub(userService, 'find') .returns({ dob: 'April 5' }); expect(getBirthday(12345)) .to.equal('April 5'); userService.find.restore(); });
Ok here we go everything is working now, well kind of, seems every time my test fails all those other tests start failing again.
If you remember from What the Heck is Mocha when an assertion fails it throws an exception, that means that when my test fails the userService.find.restore()
function never gets called.
It is a good idea to restore your stubs in an
after()
hook to ensure they always get called.
describe('Birthdays', () => { after(() => userService.find.restore()); it('should return the users birthday', () => { sinon.stub(userService, 'find') .returns({ dob: 'April 5' }); expect(getBirthday(12345)) .to.equal('April 5'); }); });
Fantastic everything is working all the time now. If my test fails it does not affect any of the other tests because I always restore my stub. Now lets add a second test
describe('Birthdays', () => { after(() => userService.find.restore()); it('should return the users birthday', () => { sinon.stub(userService, 'find') .returns({ dob: 'April 5' }); expect(getBirthday(12345)) .to.equal('April 5'); }); it('should return the users birthday month', () => { sinon.stub(userService, 'find') .returns({ dob: 'April 5' }); expect(getBirthdayMonth(12345)) .to.equal('April'); }); });
Ok well that was unexpected. What does Attempted to wrap find which is already wrapped mean?
Well like I said above sinon
decorates or wraps functions adding a bunch of properties to count invocations and inputs. When using sinon.stub()
to replace a real function with a stub sinon
is able to tell that the function your trying to stub is already a stub.
You can not stub a stub, nor can you spy a spy
Take a look at our code again, can you spot the problem? Remember that sinon.stub()
is not scoped to a test and that after()
is only called at the end of suite. Did you see it, we only restore userService.find
at the end of the suite but try to stub it out within each test.
To solve the double-wrapping issue we can use an afterEach()
instead of the after()
hook to ensure that the stub is restored after each test and not just at the end of the suite.
Alternatively since the behaviour of the stub for each test is the same we could move the stubbing out into a before()
hook thus saving us from stubbing, restoring, and then stubbing again.
describe('Birthdays', () => { before(() => ( sinon.stub(userService, 'find') .returns({ dob: 'April 5' }) )); after(() => userService.find.restore()); it('should return the users birthday', () => { expect(getBirthday(12345)) .to.equal('April 5'); }); it('should return the users birthday month', () => { expect(getBirthdayMonth(12345)) .to.equal('April'); }); });
Smashing! Now my tests are working as expected, but now I want to make sure that userService.find
was invoked with the passed in user id.
As stated above sinon.stub()
is also a spy so we have access to the same call count properties thus allowing us to check if userService.find
was called and with which parameters. I could just add an additional expect()
line within my it()
blocks but from a test organization point of view that feels a bit bad form as then either expect
can fail the test and I might not easily know which one did it; that and I’d have to update my test description to include the code smell key word “and”: should return the users birthday and look up the user by id
So I’d recommend adding a second it()
to ensure that getBirthday
calls the userServie.find
function correctly. To improve the organization of the test a bit lets add some extra describe()
blocks to group the tests together.
describe('Birthdays', () => { before(() => ( sinon.stub(userService, 'find') .returns({ dob: 'April 5' }) )); after(() => userService.find.restore()); describe('getBirthday', () => { it('should return the users birthday', () => { expect(getBirthday(12345)) .to.equal('April 5'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); describe('getBirthdayMonth', () => { it('should return the users birthday month', () => { expect(getBirthdayMonth(12345)) .to.equal('April'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); });
Ok that didn’t work, what is going on here?
First that error message isn’t very helpful now is it? It kind of goes back to what I was saying about having more then one expect in a test: I don’t know which expect just failed. Luckily I know a way to clear it up a bit.
expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true;
If you look at the test the expects are calling the decorated properties of the stub/spy and asserting them to be true, well that is why we’re seeing the error message the way we are, we only told chai
that we expect a boolean to be true and it is telling us that it got false. What we would really like to tell chai
is that we expect the function userService.find
to have been called exactly once. Well you can via a plugin for chai
called sinon-chai
. The sinon-chai plugin adds assertions to chai
which know how to access the sinon
decorated properties just like we did above but will generate more useful error messages. By installing sinon-chai
we can re-write the test to tell use more clearly what went wrong.
... const sinonChai = require('sinon-chai'); chai.use(sinonChai); ... describe('Birthdays', () => { ... describe('getBirthday', () => {...}); describe('getBirthdayMonth', () => { it('should return the users birthday month', () => { expect(getBirthdayMonth(12345)) .to.equal('April'); }); it('should look up the given user', () => { expect(userService.find).to.have.been.calledOnce; expect(userService.find).to.have.been.calledWith(12345); }); }); });
Ok well that is better, now I know that the issue is that userService.find
was called more then once. But why is it being called more then once? Remember that the stub/spy is not scoped to a test and we stub the userService.find
function once but then use it in two different tests getBirthday and getBirthdayMonth. By the time we run the last test userService.find
has already been called by the first test.
To resolve this we have a few options, we can change the before
/after
to beforeEach
/afterEach
but that wouldn’t work because we’re not invoking the functions in the should look up the given user tests.
Remember
beforeEach
/afterEach
run before/after eachit()
We could invoke the getBirthday
/getBirthdayMonth
function within the should look up the given user tests to solve that issue, which wouldn’t be a bad idea, for sure but what if we had a case where re-invoking the code for each assertion wouldn’t work, what else could we do?
We could move the before
/after
hook down into each sub testsuite. That isn’t a bad idea either but in this case does seem a bit of a duplication; however, still a valid solution.
describe('Birthdays', () => { describe('getBirthday', () => { before(() => ( sinon.stub(userService, 'find') .returns({ dob: 'April 5' }) )); after(() => userService.find.restore()); it('should return the users birthday', () => { expect(getBirthday(12345)) .to.equal('April 5'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); describe('getBirthdayMonth', () => { before(() => ( sinon.stub(userService, 'find') .returns({ dob: 'April 5' }) )); after(() => userService.find.restore()); it('should return the users birthday month', () => { expect(getBirthdayMonth(12345)) .to.equal('April'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); });
I often choose to do this as it at least keeps the tests concise, I don’t have to scroll up and down to try and figure out all the inputs and outputs are to my test suite.
Yet I’d be remiss not to point out one other option. The stub/spy decorated functions have another feature which lets you reset their state.
Each stub/spy has a function on it called reset()
which wipes out the call history and behaviours, calling this function clears the stub/spy back to factory settings if you will.
In our case that seems a bit over kill as we still want the defined behaviour, we just want to clear its call history. The good news is that each stub/spy also have a function called resetHistory()
which does exactly that, just wipes out the call history back to zero. There is also a resetBehavior()
for wiping out the behaviour while leaving the call history in place but for our needs we’ll use the resetHistory()
function.
describe('Birthdays', () => { before(() => ( sinon.stub(userService, 'find') .returns({ dob: 'April 5' }) )); after(() => userService.find.restore()); describe('getBirthday', () => { before(() => userService.find.resetHistory()); it('should return the users birthday', () => { expect(getBirthday(12345)) .to.equal('April 5'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); describe('getBirthdayMonth', () => { before(() => userService.find.resetHistory()); it('should return the users birthday month', () => { expect(getBirthdayMonth(12345)) .to.equal('April'); }); it('should look up the given user', () => { expect(userService.find.calledOnce).to.be.true; expect(userService.find.calledWith(12345)).to.be.true; }); }); });
Sláinte! All the tests now pass!
So there you have it, that is how you mock out functions when writing tests for JavaScript. You use sinon
a stand along mocking library which lets you decorate functions with spies to count how often they are invoked or to substitute them with stubs which can be setup with given behaviours to test how your code handles different responses from dependent modules.
Until next time think imaginatively and design creatively