ImaginativeThinking.ca


A developers blog

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 the userService.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 each it()

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

Brad

My interest in computer programming started back in high school and Software Development has remained a hobby of mine ever since. I graduated as a Computer Engineering Technologist and have been working as a Software Developer for many years. I believe that software is crafted; understanding that how it is done is as important as getting it done. I enjoy the aesthetics in crafting elegant solutions to complex problems and revel in the knowledge that my code is maintainable and thus, will have longevity. I hold the designation Certified Technician (C.Tech.) with the Ontario Association of Computer Engineering Technicians and Technologists (OACETT), have been certified as a Professional Scrum Master level 1 (PSM I) and as a Professional Scrum Developer level 1 (PSD I) by Scrum.org as well as designated as an Officially Certified Qt Developer by the Qt Company. For more on my story check out the about page here

Feel free to write a reply or comment.