Lessons Learned: Test-Driven Development with NodeUnit
Hi, everyone. This is James Shore with my lessons learned about test-driven development with NodeUnit. I'm recording this on August 24th, 2012.
A Complete NodeUnit Test
Let's take a look at an example test before we get into the details. You can find the source code for this episode at the link on your screen.
The production module we're testing is very simple: You initialize it with a number... and then ask it to calculate the difference with another number... but it never returns a value less than zero. (I've kept this example very simple so we can focus on the NodeUnit side of things.)
So now let's look at the tests for this code. NodeUnit follows the xUnit style of test framework. When this test runs, first
setUp() is run to initialize the production module... then we calculate the difference... and finally we assert that the value matched our expectations.
When we run this test, NodeUnit shows us the tests that ran and their results.
To run the tests from our automated build, we create a list of test files... load the NodeUnit test reporter... run the tests... and check to see if they failed.
Now let's go into more detail.
About Test-Driven Development
Test-driven development is a simple idea with powerful results. At its core, it's a way for programmers to be sure that our code does what we think it does. It also helps us improve the design of our code, document our intent, and create a comprehensive regression test suite. It's pretty awesome.
TDD works in the famous "red-green-refactor" cycle. First you write a bit of test code; then you write a bit of production code; then you refactor. I like to break it down into five steps.
First, start by thinking about what you want your code to do next. Then think of what test you could write that would fail until you write exactly the production code you want. This backwards thinking of writing tests to force you to write production code is what makes TDD "test-driven" and it's the hardest step for beginners. To make it more difficult, TDD is meant to be a rapid, fine-grained cycle, so you should only be writing a few lines of new test and production code each time through the loop. This takes a lot of practice, so take your time on this Think step.
Once you've figured out what you want to program and how you're going to test it, write the test code. It should only be a few lines of code. Run all your tests and watch the new one fail. People usually call failing tests a "red bar" because many tools show a red progress bar when tests fail.
Once the test fails, write the production code needed to make the test pass. Again, a few lines of code should be enough. Don't worry too much about writing great code--just get the test to pass. This is called "green bar."
When the test is passing, you're ready to improve the quality of the code by refactoring. Refactor as many times as you like, but again, be sure to work in small steps and run the tests after each refactoring. That way, if anything breaks, you'll know.
When the code is as clean as you know how to make it, start again with the next small expansion of your production code. Keep repeating the cycle until you've covered all the edge cases.
Test-driven development works for two reasons. First, you're expressing your intentions twice: once in your tests and once in your production code. In order for a mistake to slip through, you have to make it twice, which means that most errors are caught right away. Now, TDD won't protect you from mistaken assumptions, but it will tell you when you've programmed something differently than you meant to.
The second reason TDD works is the tiny steps. You're constantly forming and checking hypotheses. You think to yourself, "I think this test will fail in this way," and it does. And then, "I think this test will pass," and it does. You're checking your progress every few lines of code, which means you keep control of what's going on with your code.
The asynchronous approach has also led to some criticism. Node programs can be a bit difficult to read and write because program callbacks don't execute in the order that they've been written. Also, if you need to run several asynchronous operations in a row, you can end up with a mess of nested functions if you aren't careful about your design. And you can't just ignore the asynchronousity. Any function that uses an asynchronous operation becomes asynchronous itself, which means that most of your code will be asynchronous, too. This makes tools like NodeUnit more complex.
exports variable. Those exports are available to whatever code uses your module.
To use a module, use the built-in
require() function. For global or NPM modules, pass in the name of the module you want. For modules you've written yourself, pass in the relative path to the module. The
require() call returns the
exports variable of the module, which you can then call functions on.
Now let's tie it all together and test-drive a simple module using NodeUnit. In this section, we'll cover installing NodeUnit, how to write a simple test, test-driving a module, and abstracting setup and teardown.
Before we begin, we'll need to install NodeUnit. The easiest way is to use npm, the Node Package Manager. To install NodeUnit, run
npm install nodeunit.
You can install NodeUnit globally, with the
-g option, which will let you run your tests using the nodeunit command-line tool. But I prefer to install it locally and run it from my build script--that way, everything is automatically setup for other members of my team. I'll show you my build script code in a few minutes. For now, just remember that the "Jake" command runs my tests.
A Simple Test
Once NodeUnit is installed, create a test file. Most people put their tests in a
test/ directory. Personally, though, I prefer my test files to live next to my production code, since I'm always opening them at the same time. I start the test files with an underscore so they sort to the top of the directory listing, and I name them after the production file they're going to test. But you can use whatever naming scheme you want.
To write a test, export a function starting with the word
test that takes a
test parameter. When you run NodeUnit, it will automatically find and run every exported function that starts with "test." Other than that you can name the function any way you like. It's best to use names that will help other programmers understand the module you're testing.
Your tests will typically consist of three parts: first, set up the production code... then run the production code... and then check the result by asserting the actual value... the expected value... and an optional message.
To make assertions in NodeUnit, use the
test parameter passed into your test. It includes several useful assertions. They're documented in the NodeUnit README.
In addition to the standard three parts of a test (that is, setting up production code, running production code, and checking the result), NodeUnit tests have a fourth part: every NodeUnit test must end with a call to
test.done(). Remember when I said that Node uses asynchronous I/O, and that affects tools like NodeUnit? This is what I was talking about. It's possible--even common--to write tests that include an asynchronous callback, so you have to tell NodeUnit when the last callback is finished by running
test.done(). If you don't, the tests will either hang or fail outright.
Test-Driving Node.js Code
With the basics under our belt, now we can test-drive a simple module. Our module going to calculate differences against an initial value, never returning a result less than zero. (Again, I'm keeping the module simple so we can focus on how the tests work.)
We already have our first test, so let's prove that it fails... then put in some empty functions... see that it still fails with the right error... write the code to make the test pass... and prove that it did.
With that test working, we can go around the TDD cycle again. This time, let's implement the "never less than zero" part. We'll write a test that will force us to handle that case... see it fail... then write code to make it pass... and see it pass.
SetUp and TearDown
You'll often find that many of your tests have similar setup needs. You can see the duplicate setup here in the example code, although, yeah, it's a bit contrived.
NodeUnit allows you to factor out common setup code into a
setUp() function. This function will be called before every test.
setUp() function takes a callback called
done that you have to call when your setup code is done. You have to call it for the same reason you have to call
test.done() in your tests--because Node is asynchronous and this is the only way for NodeUnit to know when all the asynchronous callbacks are done.
setUp() function is optional, but it's a good way to factor out duplicate setup code in your tests. Let's use it to factor out the setup for our difference engine.
You can also write a
tearDown() function to run after every test. You won't need it as often as
setUp, but it's equally useful and it works in the same way.
You can run NodeUnit from the command-line, but I prefer to script it from my automated build. The process isn't documented, but I've found an approach that seems to work.
Here's how you run NodeUnit in a Jakefile. First we create a Jake task... with a description. NodeUnit runs asynchronously, so we have to tell Jake that our task is asynchronous. Inside the task, we'll set up a FileList with all our test files.
Now for the code to actually run NodeUnit. You can use this code anywhere, not just inside Jake.
require() NodeUnit... then get the test reporter we want. In this case, we're using the default reporter. Next, we run the reporter, which will also run the tests. We pass it the array of files we want to check... no options... and a callback for when it's done.
When NodeUnit is finished running the tests, it will call the callback. We check to see if there were any failures... and fail if there were. Then we tell Jake that the task is complete.
That's what I've learned so far about test-driving development with NodeUnit. To recap, test-driven development is a tight loop that starts with thinking about which small test will move your code forward, writing that test and seeing it fail, writing just enough production code to make the test pass, refactoring until the code is clean, and then repeating until the code is done.
NodeUnit is a TDD framework for Node.js. It's a simple framework with good support for Node's asynchronous programming model. To use it, write your test as a module. NodeUnit will automatically run any exported functions that start with the word
test. You can also run code before and after each test by exporting functions named
I've provided a script to run NodeUnit tests from a Jake build script. You can find that script and the rest of the example test code at the link on your screen.
Thanks for watching, and I'll see you next time!