Monday, 20 September 2010
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.
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:
If you have npm installed, you can install by doing:
Create a project directory and cd into it. Let's call this project 'doubled':
Let's also make a directory for our unit tests, by convention (and the commonjs specs), this is called 'test':
Now, create the file test-doubled.js in the test directory, and add the following:
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:
This tells nodeunit to run all the modules in the test directory. This should output something similar to the following:
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:
Now, run the tests again:
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:
That looks fairly all-encompassing, time to run the tests again:
Time to update our module in the familiar cycle of Test Driven Development:
Run the tests again:
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.
Next, we'll create a test for the 'read' function. This function should read a value from stdin, and call process.exit when complete:
Time to add the read function, which should open stdin, wait for data, then call process.exit:
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:
OK, we're now testing for output using console.log. Run the tests!
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:
Now, when you run your test, it should fail:
Great, let's update the read function so that it passes:
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:
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:
Now, run the tests:
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!
Phew! Right, let's try running those tests again:
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!
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:
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:
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.