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!