Skip to content

Keep calm and love JavaScript unit tests - Part 1

Thibaut Gatouillat8 min read

A few months ago, we started a project with a Node.js backend. During this project we learned how to write clean and efficient tests, and we decided to write about it.

In this first part we will present the tools we used to test our application and why they are great. We will show you examples of Node.js tests, but the libraries can be used to test both your front and backend code. All the examples in this article can be found in this repository: https://github.com/tib-tib/demo-js-tests-part-1.

Prerequisites

You must have Node.js and npm installed. To do so, you can follow the npm documentation.

Let’s write a JavaScript unit test

What might a test look like when you don’t use any framework or library? Let’s take for example a function that computes the square of a given number:

module.exports = {
  square: function(a) {
    return a*a;
  }
};

We assume that this function is in the file /workspace/math.js. You can write a test in the file /workspace/math.test.js. Since a test file is just a regular JavaScript file, you can name it as you wish though it is a good practice to use naming conventions. To check the behavior of the square function, we can use the different methods provided in the assert module available in Node.js.

var assert = require('assert');
var math = require('./math');

assert.equal(math.square(3), 9);

To launch the test, run: node /workspace/math.test.js. There is no output, that means the test succeeded because assert only provides information about the first failure. If you want a specific message you have to provide one in the argument list:

var assert = require('assert');
var math = require('./math');

assert.equal(math.square(2), 4, 'square of 2 is 4');
assert.equal(math.square(3), 9, 'square of 3 is 9');

If you make a mistake in your square function - for instance return a+a; instead of return a*a; - and launch the test, you will now see an error:

assert.js:86
  throw new assert.AssertionError({
        ^
AssertionError: square of 3 is 9
    at Object.<anonymous> (/workspace/math.test.js:5:8)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:501:10)
    at startup (node.js:129:16)
    at node.js:814:3

We can see that our test failed, but we don’t have any information about why it did.

How to have clearer output and organized tests?

Let’s now try Mocha. Mocha is a framework to run tests serially in an asynchronous environment. In your workspace, install it with the following command:

npm install mocha

Then, we have to change our test file a little bit to use Mocha’s features. Let’s create the file /workspace/test/math.js:

var assert = require('assert');
var math = require('../math');

describe('square', function() {
    it('should return the square of given numbers', function() {
        assert.equal(math.square(2), 4);
        assert.equal(math.square(3), 9);
    });
});

By default, Mocha will launch all JavaScript files located in a test directory in your workspace. This is great because we just have to run ./node_modules/.bin/mocha to handle several test files. Another benefit is that we have some output that describes our whole application:

  square
    ✓ should return the square of given numbers

  1 passing (8ms)

In case of failure we also have a much clearer output:

  square
    1) should return the square of given numbers


  0 passing (18ms)
  1 failing

  1) square should return the square of given numbers:

      AssertionError: 6 == 9
      + expected - actual

      -6
      +9

      at Context.<anonymous> (test/math.js:7:16)

We can see the tests that fail at a glance and then we have the detail of the failures. We notice that the expected and actual values are displayed, which is convenient to help us find our bug(s). Plus, we don’t have the assertion stack trace anymore, as it does not provide useful information.

Moreover, mocha allows us to have a clean and organized test structure. With describe you can literally describe what you are testing, and with the it function you can tell explicitly what behavior your function should have.

Now that we have a proper test organization, we can focus on our assertions. Indeed, they lack readability.

Write assertions like you write sentences

Chai is an assertion library that helps improving the readability of your tests in two ways. First, you can use more semantic functions like lengthOf, below or within in your assertions. Second, it provides expect and should interfaces in order to have a more human friendly syntax in our assertions. For instance, thanks to Chai you can write the following assertions:

math.square(3).should.be.above(3);
math.square(3).should.be.within(6, 12);
math.square(3).should.be.below(10);

Let’s go back to our previous example. You can install chai with the following command:

npm install chai

With chai, our workspace/test/math.js will now look like:

var should = require('chai').should();
var math = require('../math');

describe('square', function() {
    it('should return the square of given numbers', function() {
        math.square(2).should.equal(4);
        math.square(3).should.equal(9);
    });
});

And the output is a bit different in case of failure:

# Output with mocha and assert
AssertionError: 6 == 9
# Output with mocha and chai
AssertionError: expected 6 to equal 9

As of now we have everything we need to test a JavaScript file. The tests we write are easy to read and their output provides useful information in case of success as well as in case of failure. However, our square function had very few logic. What will happen if we want to test a function depending on other services?

How to handle function dependencies in your tests?

Let’s add another service, called equation.js, in our workspace. It will contain a discriminant function, that uses the square function defined in the service above.

var math = require('./math');

module.exports = {
  discriminant: function(a, b, c) {
    return math.square(b) - 4*a*c;
  }
};

Then, let’s write a test of discriminant in /workspace/test/equation.js. This test looks like the test of square:

var should = require('chai').should();
var equation = require('../equation');

describe('discriminant', function() {
    it('should return the discriminant of given numbers', function() {
        equation.discriminant(3, 2, -5).should.equal(64);
        equation.discriminant(3, 11, 7).should.equal(37);
    });
});

As we already said, when we launch the tests with mocha (./node_modules/.bin/mocha) all the files in test are used. We now have the following output:

  discriminant
    ✓ should return the discriminant of given numbers

  square
    ✓ should return the square of given numbers

  2 passing (14ms)

Our two methods are tested. That’s great!

Let’s break square and see what happens. As before we replace a*a by a+a and here is the test result:

  discriminant
    1) should return the discriminant of given numbers

  square
    2) should return the square of given numbers


  0 passing (20ms)
  2 failing

  1) discriminant should return the discriminant of given numbers:

      AssertionError: expected -62 to equal 37
      + expected - actual

      --62
      +37

      at Context.<anonymous> (test/equation.js:7:48)

  2) square should return the square of given numbers:

      AssertionError: expected 6 to equal 9
      + expected - actual

      -6
      +9

      at Context.<anonymous> (test/math.js:7:31)

The test of square fails which is the expected behavior but the test of discriminant also fails which means we did not write a unit test. The first consequence is that we don’t know where our code is broken. Is it square or discriminant that we have to fix?

To unit test the discriminant function, we have to “stub” the square function, that is to say we have to fake its behavior so that the test of the discriminant does not depend on a function of another service. We can do this with Sinon. You can install it with the following command:

npm install sinon

Then, we can modify the test file /workspace/test/equation.js:

var should = require('chai').should();
var sinon = require('sinon');

var equation = require('../equation');
var math = require('../math');

var stub;

describe('discriminant', function() {
    before(function() {
        stub = sinon.stub(math, 'square').returns(4);
    });

    after(function () {
        stub.restore();
    });

    it('should return the discriminant', function() {
        equation.discriminant(3, 2, -5).should.equal(64);
        equation.discriminant(3, 11, 7).should.equal(-80);
    });
});

You see two functions before and after. These functions define what to do before and after launching the tests. Here, we initialize the stub in the before function, and we restore the initial behavior of square in the after function. After defining a stub, it is very important to restore it, because otherwise the stub will be active in following tests, and thus will break them.

The stub allows us to define a fake return value for the square function. It means that whenever this function is called, it will return the value 25. As a consequence, when we call the discriminant function with the a, b and c parameters, the b value won’t be used because the stub returns a specific value.

Now if square is broken, we have the following output:

  discriminant
    ✓ should return the discriminant

  square
    1) should return the square of given numbers


  1 passing (28ms)
  1 failing

  1) square should return the square of given numbers:

      AssertionError: expected 6 to equal 9
      + expected - actual

      -6
      +9

      at Context.<anonymous> (test/math.js:7:31)

The test of discrimant is ok which is what we want since there is no error in the discrimant function. The test of square is failing which gives us a good idea of where we made a mistake in our code.

The stub allows us to isolate the logic of the discriminant function, and that’s why Sinon is very useful.

What’s next?

Now that you understand the purpose of each library of the Mocha-Sinon-Chai stack, it is time to write some more complex tests. It will be the subject of the second part of our tutorial, in which you will learn about sandboxes, tests on functions using callbacks, or using promises among many other things. Be ready!