An intro to testing in Node

Motivation

This blog post serves as a practical introduction to testing your Node.js code. If you're wondering why you should spend your valuable development time writing tests, read this and this. In short, tests save you from avoidable bugs that take up many frustrating hours to find and fix. They also give you the confidence to refactor and improve your existing code without fear of breaking existing functionality.

It is especially important to write tests for loosely-typed, dynamic languages such as JavaScript. The power JavaScript gives us is offset by the ease with which we can screw things up. For example, in Java, if we want to multiply two numbers together, we can use a function like this:

public static int multiply(int a, int, b) { return a * b; }

Simple!

Java protects us from ourselves - it doesn't allow a or b to be anything other than ints and it does not allow us to return anything other than an int.

An equivalent JavaScript method may look like this:

function (a, b) { return a * b; }

Here's where things could go wrong: JavaScript does not require that a and b be numbers; they could be objects, or strings, or not even exist. If we change this method to return a string instead (which we can do without changing the function signature), JavaScript doesn't care - it'll let us do that.

Tests in JavaScript and Node.js are vital.

Tools

Node development is modular. We have a number of tools tackling problems with testing. I've listed my favorite stack here:

In classic Node style, we don't have one end-all-be-all solution for testing - instead we have a number of libraries that each tackle one specific problem.

We use Mocha to set up and run our tests.

/* global describe, it */ var assert = require('chai').assert; var multiply = require('/path/to/multiply'); describe('multiply', function() { it('should multiply two numbers together', function () { assert.equal(20, multiply(4, 5)); }); });

Notice that we needed a second library just to run the tests - Mocha doesn't provide assertions. This is where Chai comes in. Chai lets us write assertions in a human, readable way. Its API gives us the power to write comprehensive tests.

var expect = chai.expect; var value = 23; expect(value).to.equal(23); expect(value).should.be.a('number');

If you have purely functional JavaScript that has no dependencies, then you're done - and bravo, I might add! However, most real life applications have dependencies. In order to create robust tests, we have two requirements for dependencies:

  1. A way to make sure they're used correctly.
  2. A way to stub their output.

Sinon provides this functionality.

/* global describe, it */ var assert = require('chai').assert; var expect = chai.expect; var sinon = require('sinon'); describe('useful math', function () { describe('multiplyByRandom', function() { it('gets random number from generateRandom', function() { var usefulMath = require('/path/to/usefulMath'); var generateRandomSpy = sinon.spy(); // Where generateRandom is the existing method we expect // multiplyByRandom to call. usefulMath.generateRandom = generateRandomSpy; usefulMath.multiplyByRandom(2); assert(generateRandomSpy.calledOnce); }); it('multiplies parameter by random number from generateRandom', function() { var randomNumber = 32; var testParameter = 2; var usefulMath = require('/path/to/usefulMath'); var generateRandomStub = sinon.stub().returns(randomNumber); usefulMath.generateRandom = generateRandomStub; var result usefulMath.multiplyByRandom(testParameter); expect(result).to.equal(randomNumber * testParameter); }); }); });

Great! Now we can write tests not just for our pure functions, but also for modules that have internal dependencies.

But what if we have external dependencies? We can't simply set our Sinon stub/spy to the functions on our require'd modules, since that require is opaque to anything outside of our module. This is where our final library, Mockery, comes into play. Mockery can stub require and return whatever we want instead of the original dependency.

/* global describe, it, before, after */ var mockery = require('mocha'); var assert = require('chai').assert; var sinon = require('sinon'); var testNumber = 33; var fakeNumberGenerator = sinon.stub.returns(testNumber); // This is a mocha function that is called before all the tests are run before(function() { mockery.enable(); mockery.register('/path/to/numberGenerator', fakeNumberGenerator); }); describe('usefulMath', function() { describe('multiplyByOutsideNumber', function() { it('multiplies parameter by number from numberGenerator', function() { var usefulMath = require('/path/to/usefulMath'); var testParameter = 9; var result = multiplyByOutsideNumber(testParameter); expect(result).to.equal(testParameter * testNumber); }); }); )}; // This is a mocha function that is called after all the tests are run after(function() { mockery.disable(); });

Using these four tools, we can create a complete test for most applications.

Write Testable Code

The gurus at Google have written an excellent guide to writing testable code. Three qualities I suggest you strive for in your code are:

  • Design small, single purpose units of code
  • Limit reliance on state (write pure functions)
  • Don't make undocumented assumptions about your input and output

TL;DR

Writing tests is vital for stable, long-lived applications. With Node.js and NPM, we have access to powerful tools that help us create comprehensive, useful tests. But in order to test effectively, we must first write code that is conducive to testing.

Share This