Getting to 99.9% test coverage with Mocks, Stubs & Spies

Full test coverage
Quality is never an accident; it is always the result of intelligent effort. — John Ruskin

In this tutorial, we’ll learn how to raise the bar of our test coverage by taking a simple app from good to excellent coverage. But first, let’s understand some terminologies.

Mocks

Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations. — SinonJS

Stubs

These are functions with preprogrammed behavior. Stubs allow us to (temporarily) take control of real components (other functions or methods) and return responses from those components any way we choose.

Simply put, stubs simulates behaviors. An example use case for stub is when we want to fake a database query response.

Spies

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls — SinonJS

With that out of the way, we can now begin writing code.

Let's get started!

Because this is a guide on increasing test coverage, our focus won’t be on writing the actual code but to write tests that cover code we’ve already written. I’ve put together a simple app for us to work with. Fire up your terminal and let’s get cracking.

git clone git@github.com:codeshifu/awesome-test-coverage.git
cd awesome-test-coverage
npm install

Next, we’ll check the current test coverage of the app by running npm testin the terminal. You should see something similar to the screenshot below.

Initial test coverage

Not so bad, but we can do better. Let’s try and understand why we’re not hitting 100% on all of the files above. Locate inside the coverage folder, index.html and open it in your preferred browser. The file represents the coverage report of our test generated by nyc.

test coverage report generated by nyc

We’ll start with the controller. Click on the src/controllers link and then click the index.js file

src/controllers/index.js

In this file, you can see some red lines. These are lines/statements not covered by our test and interestingly enough, they can only be covered by our test if there’s a way to simulate a server error.

Simulating server error

Many of the tests I’ve written in the past never cover for when something goes wrong with the server. With SinonJS, it becomes easier to test for this edge case. Sinon has been included in the repo we cloned above and ready for use.

Inside the test directory, locate the cats.spec.js file and open it in your preferred code editor. For simplicity, this will be the only test file we’re going to be working with. Add the code below inside cats.spec.js

it('fakes server error getting all cats', async () => {
const req = {};
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
sinon.stub(db, 'get').throws();
    await CatsController.findAll(req, res);
expect(res.status).to.have.been.calledWith(500);
});
});

We have a few things going on here. Let’s break them down. The CatsController.findAll takes two arguments which normally, is automagically passed to the function by the Express server whenever a route using that function is hit.

  1. We construct our own custom request & response object and pass them to CatsController.findAll(req, res)
  2. sinon.stub(res, 'status').returnThis() , here we stub a method statuson the res object and we tell it to return an instance of the object it belongs to. In this case, it returns back the res object. We did this so we can chain on other methods on the res object e.g. res.status(404).send('whoops!')
  3. sinon.stub(db, 'get') stubs the get method on the db object and forces it to throw an error whenever db.get(‘something') is called. We are saying, hey! don’t do what you’d normally do when db.get is called, instead, I want you to throw an error.
  4. Then we call the CatsController.findAll method, passing in the req & res object we constructed earlier. Because we have stubbed the get method of the db object, when the CatsController.findAll runs, and inside of it, there’s a call to db.get(), it’ll throw an error.
  5. Lastly, we assert that res.status must have been called with 500 as the argument.
it('fails to find cat by ID', async () => {
const req = { params: { id: 1 } };
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
sinon.stub(db, 'get').returnsThis();
sinon.stub(db, 'find').returnsThis();
sinon.stub(db, 'value').returns(false);
    await CatsController.findOne(req, res);
expect(res.status).to.have.been.calledWith(404);
});

A few things added to our req object here, and this is because the method CatsController.findOne that uses it, requires that there should be a params property on it. We always construct the req object in a way the function that uses it does. Next, stub db.value so that it returns false even if the params.id in the req object is valid. This helps simulate a 404 scenario.

it('fakes server error getting a cat by ID', async () => {
const req = { params: { id: 1 } };
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
sinon.stub(db, 'get').returnsThis();
sinon.stub(db, 'find').throws();
    await CatsController.findOne(req, res);
expect(res.status).to.have.been.calledWith(500);
});

By now, you should understand what’s going on here. We simulate a server error when trying to find a cat by it’s ID.

it('fakes server error creating a cat', async () => {
const req = {
body: {
name: 'fido',
age: 3
}
};
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
sinon.stub(db, 'get').throws();
    await CatsController.create(req, res);
expect(res.status).to.have.been.calledWith(500);
});
it('sends 400 when cat name or age is/are not provided', () => {
const req = { body: {} };
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
const next = () => {};
    validateCat(req, res, next);
expect(res.status).to.have.been.calledWith(400);
});
it('sends 403 if cat age is > 18', () => {
const req = { body: { name: 'stuppy', age: 22 } };
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
const next = () => {};
    validateCat(req, res, next);
    expect(res.status).to.have.been.calledWith(403);
});

Spying on functions

We can spy on functions to observe their interaction with tested code. Spies allows us to verify the number of times a function has been called, how it was called (with what arguments) and what it responded with.

it('spy on next()', () => {
const req = { body: { name: 'stuppy', age: 16 } };
const res = {
status() {},
send() {}
};
    sinon.stub(res, 'status').returnsThis();
const next = sinon.spy();
    validateCat(req, res, next);
    expect(next).to.have.been.calledOnce;
});

Here, we created a spy called next and pass it as an argument to validateCat function and we assert that next must have been called exactly once inside validateCat

If you run npm test again, you should now have all statements, branches, lines, and functions covered.

What’s next?

Now that you have an understanding of how to increase your test coverage using SinonJS, I suggest you give the documentation a read because the possibilities are limitless.

P.S I’ve intentionally left out mocks because I couldn’t find a good use case for it in this app.

There you have it, getting to 99.9% test coverage with mocks, stubs & spies. I hope this helps someone… and until next time, happy coding.


Do you need to hire top developers? Talk to Andela to help you scale
Are you looking to accelerate your career as a developer? Andela is currently hiring senior developers.
Apply now.