Unit testing in node.js

Node.js provides a its own assert module with some really useful functions for creating basic tests. However, the reporting and running of these assertions can become complicated, especially with asynchronous code. How can you be sure that all assertions ran? Or that they ran in the correct order? This is where nodeunit comes in, a tool for defining and running unit tests in the simplest way possible.

Lets walk through a basic node.js program using Test Driven Developement (TDD) and nodeunit. This program asks the user to enter a number, doubles it and returns the result.

Installing nodeunit

First of all, download nodeunit. If you want to use the absolute latest version you can clone from the github repository.

Once downloaded, extract the files, cd to the directory and run:

make && sudo make install

If you have npm installed, you can install by doing:

npm install nodeunit

Writing our first unit test

Create a project directory and cd into it. Let's call this project 'doubled':

mkdir doubled
cd doubled

Let's also make a directory for our unit tests, by convention (and the commonjs specs), this is called 'test':

mkdir test

Now, create the file test-doubled.js in the test directory, and add the following:

var doubled = require('../lib/doubled');

exports['calculate'] = function (test) {
    test.equal(doubled.calculate(2), 4);
    test.done();
};

Say hello to our first unit test! So, what's going on here? First, we import our (non-existent) module 'doubled'. We then export a function called 'calculate'. Nodeunit will run any exported functions as unit tests.

The test function accepts one argument, which is the test object. The test object contains all the assert module methods plus two new methods: 'expect' and 'done'. Expect can be used to tell nodeunit how many assertions you expect to run (we'll come to this later), and the done function tells nodeunit that the test has completed. Explicitly ending your unit tests is important when testing async code. Otherwise, its easy for callbacks not to fire, and for a test to end without all the assertions being run.

Inside our test, we call a function from the doubled module called 'calculate'. This was simply chosen because it seemed like a good starting point. Now, let's try running our test. From your project directory run:

nodeunit test

This tells nodeunit to run all the modules in the test directory. This should output something similar to the following:

Screen 1

I suppose we better add that doubled module then. Create a 'lib' folder in the project directory and add a doubled.js file containing the following:

exports.calculate = function (num) {
    return num * 2;
};

Now, run the tests again:

Screen 2

Much better. Now, let's say we want the function to throw an error if the argument is not a number. Time to revisit our unit test:

var doubled = require('../lib/doubled');

exports['calculate'] = function (test) {
    test.equal(doubled.calculate(2), 4);
    test.equal(doubled.calculate(5), 10);
    test.throws(function () { doubled.calculate(); });
    test.throws(function () { doubled.calculate(null); });
    test.throws(function () { doubled.calculate(true); });
    test.throws(function () { doubled.calculate([]); });
    test.throws(function () { doubled.calculate({}); });
    test.throws(function () { doubled.calculate('asdf'); });
    test.throws(function () { doubled.calculate('123'); });
    test.done();
};

That looks fairly all-encompassing, time to run the tests again:

Screen 3

Time to update our module in the familiar cycle of Test Driven Development:

exports.calculate = function (num) {
    if (typeof num === 'number') {
         return num * 2;
    }
    else {
        throw new Error('Expected a number');
    }
};

Run the tests again:

Screen 4

Success! We've now completed a basic unit test and written a module that satisfies it. Time to move onto something a little more challenging and look at reading user input.

Testing asynchronous code

Next, we'll create a test for the 'read' function. This function should read a value from stdin, and call process.exit when complete:

exports['read a number'] = function (test) {
    var ev = new events.EventEmitter();

    process.openStdin = function () { return ev; };
    process.exit = test.done;

    doubled.read();
    ev.emit('data', '12');
};

This test does something JavaScript is very nice for, stubs and mock objects! Firstly, stdin is an EventEmitter, so we create our own EventEmitter object and override process.openStdin to return it. Next we override process.exit with the test.done function, so this test will only complete successfully if the read function calls process.exit. Next, we call the read function we're testing. This should wait for stdin to emit a 'data' event before doing anything. This is where our custom EventEmitter comes in handy. Now, we can use it to emit data events to simulate a user entering input.

Run the tests:

Screen 5

Time to add the read function, which should open stdin, wait for data, then call process.exit:

exports.read = function () {
    var stdin = process.openStdin();

    stdin.on('data', function (chunk) {
        process.exit();
    });
};

Try again:

Screen 6

Success! But wait, we've forgotten to do anything with the data (in fact, the read function could just be calling process.exit and still pass). We want this function to output the doubled number using console.log. Time to revisit the tests:

exports['read a number'] = function (test) {
    var ev = new events.EventEmitter();

    process.openStdin = function () { return ev; };
    process.exit = test.done;

    console.log = function (str) {
        test.equal(str, 'doubled: 24');
    };

    doubled.read();
    ev.emit('data', '12');
};

OK, we're now testing for output using console.log. Run the tests!

Screen 7

Hmm, that should have failed. We've fallen for a normal problem with unit testing, that is especially important to watch out for when testing asynchronous code. The console.log function is never called, so our 'equal' assertion never runs. What we need to do is assert that all our assertions run! Thankfully, nodeunit provides an easy way to catch this problem in most situations: the expect method. Using the expect method tells nodeunit how many assertions should have run in your test. Lets update our test function to include it:

exports['read a number'] = function (test) {
    test.expect(1);
    var ev = new events.EventEmitter();

    process.openStdin = function () { return ev; };
    process.exit = test.done;

    console.log = function (str) {
        test.equal(str, 'doubled: 24');
    };

    doubled.read();
    ev.emit('data', '12');
};

Now, when you run your test, it should fail:

Screen 8

Great, let's update the read function so that it passes:

exports.read = function () {
    var stdin = process.openStdin();

    stdin.on('data', function (chunk) {
        var num = parseFloat(chunk);
        var result = exports.calculate(num);
        console.log('doubled: ' + result);
        process.exit();
    });
};

Screen 9

Shared state and sequential testing

Currently our read function seems to work with the expected input data. But what should happen if the user enters a value other than a number? Let's write another test that checks that any errors which occur during calculating the result are displayed to the user. Here is the full test-doubled.js file after adding this test:

var doubled = require('../lib/doubled'),
    events = require('events');


exports['calculate'] = function (test) {
    test.equal(doubled.calculate(2), 4);
    test.equal(doubled.calculate(5), 10);
    test.throws(function () { doubled.calculate(); });
    test.throws(function () { doubled.calculate(null); });
    test.throws(function () { doubled.calculate(true); });
    test.throws(function () { doubled.calculate([]); });
    test.throws(function () { doubled.calculate({}); });
    test.throws(function () { doubled.calculate('asdf'); });
    test.throws(function () { doubled.calculate('123'); });
    test.done();
};

exports['read a value other than a number'] = function (test) {
    test.expect(1);
    var ev = new events.EventEmitter();

    process.openStdin = function () { return ev; };
    process.exit = test.done;
    doubled.calculate = function () {
        throw new Error('Expected a number');
    };
    console.log = function (str) {
        test.equal(str, 'Error: Expected a number');
    };

    doubled.read();
    ev.emit('data', 'asdf');
};

exports['read a number'] = function (test) {
    test.expect(1);
    var ev = new events.EventEmitter();

    process.openStdin = function () { return ev; };
    process.exit = test.done;
    console.log = function (str) {
        test.equal(str, 'doubled: 24');
    };

    doubled.read();
    ev.emit('data', '12');
};

Be sure to copy the above code into your test-doubled.js file and pay special attention to the ordering of the tests. Next, we update lib/doubled.js to catch exceptions and report them using console.log:

exports.read = function () {
    var stdin = process.openStdin();

    stdin.on('data', function (chunk) {
        var num = parseFloat(chunk);
        try {
            var result = exports.calculate(num);
            console.log('doubled: ' + result);
        }
        catch (e) {
            console.log(e);
        }
        process.exit();
    });
};

Now, run the tests:

Screen 10

Hmm, this is an odd one. The new test passes, but a previously passing test now fails. This is due to our mocking and stubbing in the tests for the read function. The problem is that node.js contains a module cache that will persist our changes to process, double and console functions. In the test 'read a value other than a number' we override double.calculate, but never restore it.

To fix this, we need to reset all these functions to their original state after testing. We need to be careful that this is done just before we call test.done(), to make sure they are not reset part-way through the test!

exports['read a value other than a number'] = function (test) {
    test.expect(1);
    var ev = new events.EventEmitter();

    var _openStdin = process.openStdin;
    process.openStdin = function () { return ev; };

    var _exit = process.exit;
    process.exit = function () {
        // reset all the overidden functions:
        process.openStdin = _openStdin;
        process.exit = _exit;
        doubled.calculate = _calculate;
        console.log = _log;

        test.done();
    };

    var _calculate = doubled.calculate;
    doubled.calculate = function () {
        throw new Error('Expected a number');
    };

    var _log = console.log;
    console.log = function (str) {
        test.equal(str, 'Error: Expected a number');
    };

    doubled.read();
    ev.emit('data', 'asdf');
};

exports['read a number'] = function (test) {
    test.expect(1);
    var ev = new events.EventEmitter();

    var _openStdin = process.openStdin;
    process.openStdin = function () { return ev; };

    var _exit = process.exit;
    process.exit = function () {
        // reset all the overidden functions:
        process.openStdin = _openStdin;
        process.exit = _exit;
        console.log = _log;

        test.done();
    };

    var _log = console.log;
    console.log = function (str) {
        test.equal(str, 'doubled: 24');
    };

    doubled.read();
    ev.emit('data', '12');
};

Phew! Right, let's try running those tests again:

Screen 11

This is also the main reason the nodeunit runs unit tests sequentially, in series. Otherwise you wouldn't be able to use any mocks, as functions would be overwriting each others mocked functions. Running tests in parallel may be a good idea for speeding up integration tests, but its a bad idea for unit testing!

Test cases, setUp and tearDown

This section requires you to have nodeunit installed in your path (by copying the nodeunit folder to ~/.node_libraries or installing it via npm)

There's a few problems with the tests above. Firstly, they repeat code which we'd rather write once. Secondly, they're ugly! This can be remedied by using test cases. The nodeunit module exports a function called testCase that helps you to define test functions that share code regarding their running environment. Let's use testCase to tidy up our test functions:

var nodeunit = require('nodeunit');


exports['read'] = nodeunit.testCase({

    setUp: function () {
        this._openStdin = process.openStdin;
        this._log = console.log;
        this._calculate = doubled.calculate;
        this._exit = process.exit;

        var ev = this.ev = new events.EventEmitter();
        process.openStdin = function () { return ev; };
    },
    tearDown: function () {
        // reset all the overidden functions:
        process.openStdin = this._openStdin;
        process.exit = this._exit;
        doubled.calculate = this._calculate;
        console.log = this._log;
    },

    'a value other than a number': function (test) {
        test.expect(1);

        process.exit = test.done;
        doubled.calculate = function () {
            throw new Error('Expected a number');
        };
        console.log = function (str) {
            test.equal(str, 'Error: Expected a number');
        };
        doubled.read();
        this.ev.emit('data', 'asdf');
    },

    'a number': function (test) {
        test.expect(1);

        process.exit = test.done;
        console.log = function (str) {
            test.equal(str, 'doubled: 24');
        };
        doubled.read();
        this.ev.emit('data', '12');
    }

});

That looks better. The setUp and tearDown functions share context with the test function, which means anything you store on the object 'this' will be available between them. Let's run the tests one more time:

Screen 12

What next?

Have fun writing unit tests and playing with node.js. For more information on nodeunit, see the README.

Be sure to watch nodeunit on github for updates! You might also want to check out the development of nodeunit-dsl by gerad, which attempts to implement a 'pretty dsl on top on nodeunit'.

If you have any problems with this guide, please comment. If you have any problems with nodeunit, please raise an issue on github.

comments powered by Disqus