Skip to content

cjohansen.no

Test driven development with JavaScript

In a series of three articles I am going to walk you through the what's and why's of unit testing and test driven development focusing on JavaScript. We'll look into the underlying concepts, available frameworks for JavaScript, and of course a practical example to top it all off.

The following is an introduction to the basic concepts. It is by no means exhaustive, but you'll find links to more resources should you require it.

Unit testing

Unit testing is a method of testing that verifies the individual units of source code are working properly.

In other words: code that tests other code. The obvious advantage of focusing your testing in writing unit tests (as opposed to ad hoc testing in a browser and/or console) is that the unit test may be run over and over - in any browser. And you only have to do the hard work once. As your code base grows this will become increasingly important as it allows you to easily verify that new code and changes don't break existing code.

As you get better at covering your code with tests you'll be able to dramatically reduce the amount of time you waste hammering F5 in your browser, or scattering console.logs all over the place. The time you spend testing will simply be a better investment, because it can be repeated indefinately with no extra effort.

A "unit"

A unit is understood as the smallest testable piece of code in your program. In most cases a unit is function/method. The test itself is often also a method (or function), so a single unit test is often one method testing another - however, the function being tested is the actual unit.

Sometimes we write several unit tests for the same method or object. This allows us to specialize each test so it tests a specific behaviour. The tests that target some closely related bunch of code (like an object with several methods) is often referred to as a test case. Test cases are often grouped together as test suites.

Code coverage

Code coverage is a way of measuring how thouroughly a piece of source code is tested by a test suite. A test suite should Ideally run each and every line of code. This includes calling all the methods, covering all branches in conditionals and more. We usually define coverage criteria, and once more I quote Wikipedia on the issue:

These are some criteria you could use to measure the coverage of your code, but that doesn't necessarily mean that you should always have 100% coverage. Path coverage is an example of a criteria which may often be impossible to cover 100%. Code coverage is usually carried out by software that analyzes your code and tests.

A simple unit test

Now we know what a unit test is. Let's take a look at what it may look like.

function add(a, b) {
    return a + b;
}

// Test function
if (add(1, 2) !== 3) {
    document.write('<p style="color: red;">add(1, 2) does not add up to 3</p>');
} else {
    document.write('<p style="color: green;">add(1, 2) OK</p>');
}

if (add("2", 2) !== 4) {
    var msg = "Numbers as strings don't add";
    document.write('<p style="color: red;">' + msg + '</p>');
} else {
    document.write('<p style="color: green;">add("2", 2) OK</p>');
}

Running this (copy and paste into Firebug and hit Run. Or run mine) yields a green message (that's good, right?) and a red message. This example is extremely over-simplified, but it illustrates nicely the basic concepts:

If a test fails we need to fix the failing code and try again:

function add(a, b) {
    return parseInt(a) + parseInt(b);
}

// Test function
if (add(1, 2) !== 3) {
    document.write('<p style="color: red;">add(1, 2) does not add up to 3</p>');
} else {
    document.write('<p style="color: green;">add(1, 2) OK</p>');
}

if (add("2", 2) !== 4) {
    var msg = "Numbers as strings don't add";
    document.write('<p style="color: red;">' + msg + '</p>');
} else {
    document.write('<p style="color: green;">add("2", 2) OK</p>');
}

Our tests now pass, and is by definition ok according to our test criteria.

You'll notice how the above example has a lot to gain by extracting the "framework" which checks the values, displays the messages and so on. Luckily we never code unit tests in such an error prone and inefficient way, we use unit testing frameworks. These frameworks differ alot in style, but as a bare minimum they provide a set of assertions - test functions that allow us to make certain assertions about our code (see how that flows naturally?) is working. If the test passes, it does.

Beyond unit tests there are integrations tests - which tests that (well tested) units actually work together, and lots of other ways to test. I'll keep my focus on unit testing for now.

Test driven development

Using unit tests as our tool, we're now ready to turn our work flow completely upside down and embrace the concept of test driven development. Test driven development is a circular process and can be described as follows:

  1. Write a test for a new feature
  2. Run the test, verify that it fails (hey, you didn't even write the code yet)
  3. Write the simplest solution that satisfies the test
  4. Refactor - clean up and polish your work
  5. Lather, rinse, repeat

When doing test driven development we start by describing - through tests - how we want our code to work. When we're happy with how it works we can start worrying about implementation details. This means that your tests become an integral part of your code design. Compared to more or less ad hoc designing as well as ad hoc testing, this way of thinking about your code is bound to improve the quality on both your APIs and your code.

Test driven development facilitates high focus on he API instead of the implementation details.

Added value for JavaScript

One of the hardest aspects of developing JavaScript is making it cross browser - working close to identical in wildly heterogeneous (and even unstable) environments. This causes you, the developer, alot of stress in two ways:

Unit testing will help you out here in a way that is unique for JavaScript. While it is definitely true that unit tests helps you verify that code will run on different implementations in other languages as well (for instance Ruby 1.8 code on Ruby 1.9), JavaScript is special because it's expected to work in alot of different environments.

Writing unit tests that can be run again whenever gives you, the developer, more freedom to choose your favorite browser and stick to it, thus improving your work flow. Because the unit tests capture all the steps from start to finish, you can just as well run them all on IE when you're done. This way you can iron out 10 ridiculous IE bugs in a single sitting instead of having your work flow continuously interrupted by small hiccups from this and that browser.

Testing is hard, and it takes time

So you don't want to do it? Well, it is true that testing is hard to get right. However, it shouldn't take you too long to get into the habit and way of thinking enough to start hating that non-reproducable ad hoc testing you used to rely on. You'll notice that you'll increase the quality of both your APIs and your code, as well as being able to rest assured that 50% of your code base does what it should.

Someone once said that testing is a muscle - you need to exercise it. And practice we will. Part two of this article will deal with some frameworks, and then we'll do a practical example to top it off.