Alisdair McDiarmid is a senior software engineer based in Toronto, Ontario.

Simple Node.js tests with assert and mocha

I've been writing some Node.js modules recently. Getting started with testing in Node was a bewildering experience. There are so many choices for test runners and assertion libraries, and they all seem really complex. As an Ember developer, I wanted to use qunit, but it doesn't really work well with Node modules.

After trying lots of options, I've settled on something really easy to get started with, and simple to understand. My favourite way to test is using assert to write tests, and mocha to run them.

Example

Here's a simple Node module we want to test, in lib/maybe-first.js:

module.exports = function maybeFirst(array) {
  if (array && array.length) {
    return array[0];
  }
};

And here's the corresponding test code, in test/maybe-first-test.js:

var assert = require('assert');
var maybeFirst = require('../lib/maybe-first');

describe('maybeFirst', function() {
  it('returns the first element of an array', function() {
    var result = maybeFirst([1, 2, 3]);

    assert.equal(result, 1, 'maybeFirst([1, 2, 3]) is 1');
  });
});

Install mocha with npm install -g mocha, and run the test with mocha:

  maybeFirst
    ✓ returns the first element of an array


  1 passing (9ms)

Read on for my reasoning for this setup, and a few more example tests. Or check out the code on GitHub.

Why assert?

There are lots of fancy assertion libraries for node. The one I see most commonly used is chai, which has tons of features, including Behaviour Driven Development-style assertions. This leads to test code that looks like this:

var beverages = { tea: ['chai', 'matcha', 'oolong'] };
beverages.should.have.property('tea').with.length(3);

This is really cool! But it's also a lot to learn, and it can be hard to understand when you first come across it.

Here's how I'd write this test in the much simpler assert style:

var beverages = { tea: ['chai', 'matcha', 'oolong'] };
assert.equal(beverages.tea.length, 3);

All you have to learn is the tiny number of assertions, and the rest is simple JavaScript.

Why not BDD?

I've written a lot of BDD-style tests in my life, most of which were in rspec. They're fun to write, and the resulting tests read almost like English. But I've also found that the tests require much more effort to come back to, and there's a huge context switch between normal code and the domain-specific-language for testing.

Switching modes like this between testing and development slows me down. I want to think as little as possible about the language I'm writing my tests in. I'd much rather use that brain energy on coming up with well-thought-through tests, or writing simple and clear production code.

Why not chai.assert?

Chai also comes with some neat extensions to the built-in assert module. I don't want to use these either.

The Node assert module is built in, and it has everything you need to write tests. The extra assertions in chai-assert are cute, but they're not necessary. Trying to remember the name of all of these assertions is more work than just writing the equivalent test code yourself.

Why mocha?

I haven't found any simpler, better test runner than mocha. It automatically finds tests, allows you to describe modules and test cases, has great reporter output, and supports async tests through callbacks or promises.

More test assertions

There are a few more useful test assertions provided by the assert module.

The simplest assertion is assert.ok, which performs a truthiness check on the value. You'll only want to use this when you don't care about the specific value of a result, only that it's not falsy.

We've already seen assert.equal, which runs a loose equality check. assert.strictEqual is probably safer, as it runs a strict equality check.

For checking arrays or objects, assert.deepEqual is really useful. It checks recursively, so you can use this to validate whole trees of JSON, for example. Note that it uses loose equality checking.

Finally, assert.throws is for checking for errors raised by functions. It calls the passed function and expects it to throw an exception. You can even assert which exception is thrown by passing a second argument.

And there are also the inverses of most of these: notEqual, notStrictEqual, notDeepEqual, doesNotThrow. They do what you'd expect!

Examples

Using assert and mocha is fairly simple once you get started, but it's always helpful to have some examples to crib from. Here are a few more: using strictEqual, testing async functions with callbacks, and testing with promises.

Example: strictEqual

What happens when you pass an empty array to maybeFirst?

> maybeFirst([])
undefined

So let's write a test for that. We're going to use strictEqual this time, since we want the test to be equivalent to JavaScript's === strict equality operator, not the default coercive ==.

  it('returns undefined if array is empty', function() {
    var result = maybeFirst([]);

    assert.strictEqual(result, undefined, 'maybeFirst([]) is undefined');
  });

Example: async test with callback

Lots of Node functions don't return anything useful, instead giving their results via callback functions. How do we test this?

Here's a ridiculous example:

function delayedMap(array, transform, callback) {
  setTimeout(function() {
    callback(array.map(transform));
  }, 100);
}

One simple approach is to assert within the callback function:

it('eventually returns the results', function() {
  var input = [1, 2, 3];
  var transform = function(x) { return x * 2; };

  delayedMap(input, transform, function(result) {
    assert.deepEqual(result, [2, 4, 6]);
  });
});

And it works:

  delayedMap
    ✓ eventually returns the results


  1 passing (8ms)

Looks great, right? But wait, how does it complete in only 8ms?

Well, let's be good unit testers and make sure that our test fails if we break our function:

function delayedMap(array, transform, callback) {
  setTimeout(function() {
    // callback(array.map(transform));
  }, 100);
}

Run mocha:

  delayedMap
    ✓ eventually returns the results


  1 passing (7ms)

Uh. Hm. What's happening here?

Mocha is marking the test as passed because no assertions failed. Unfortunately, that's because no assertions ran! So we need to make it wait until we're done.

We do that by using the done callback parameter which mocha passes to every test. Let's update our test like this:

it('eventually returns the results', function(done) {
  var input = [1, 2, 3];
  var transform = function(x) { return x * 2; };

  delayedMap(input, transform, function(result) {
    assert.deepEqual(result, [2, 4, 6]);
    done();
  });
});

Rerunning mocha:

  delayedMap
    1) eventually returns the results


  0 passing (2s)
  1 failing

  1) delayedMap eventually returns the results:
     Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

That's more like it! And now if we fix our code under test again:

  delayedMap
    ✓ eventually returns the results (109ms)


  1 passing (117ms)

Awesome.

Example: async test with promises

But callbacks are so old hat. What about testing promises? Let's rewrite our useless delayedMap function into a still-useless promiseMap equivalent:

function promisedMap(array, transform) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve(array.map(transform));
    }, 100);
  });
}

To write the test for this, make sure that your it block returns a promise, and mocha will take care of the rest:

  it('eventually returns the results', function() {
    var input = [1, 2, 3];
    var transform = function(x) { return x * 2; };

    return promisedMap(input, transform).then(function(result) {
      assert.deepEqual(result, [2, 4, 6]);
    });
  });

It works!

  promisedMap
    ✓ eventually returns the results (101ms)

  1 passing (106ms)

We don't need to call the done function to let mocha know that the test is complete. Returning a promise allows mocha to wait for it to resolve (or reject), and so we can write beautiful promise-style code without worrying about callbacks. Yay!

Careful: done and promises don't mix. If you accept the done parameter in a test which returns a promise, and don't call done at the end of your promise chain, mocha will explode. Here, look:

  it('eventually returns the results', function(done) { // <- we accept the done parameter
    var input = [1, 2, 3];
    var transform = function(x) { return x * 2; };

    return promisedMap(input, transform).then(function(result) {
      assert.deepEqual(result, [2, 4, 6]);
    });
  });

This puts mocha into "pending" mode, waiting for done to be called. The result is a failing test:

  promisedMap
    1) eventually returns the results


  0 passing (2s)
  1 failing

  1) promisedMap eventually returns the results:
     Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

So don't do that.

Finally!

I'm really happy with this way of writing tests. mocha is a great test runner, requiring hardly any setup, and supporting everything you need to write Node tests. assert is the easiest to understand assertion library I know of, and making tests easy to read and write is really important.

Here are some more links for extra learning: