Test Driven Development in Javascript with Node, Grunt, Mocha, and Chai

This is going to be a simple demonstration of using Test Driven Development practices to solve a toy problem.

We will use

  • Node for running Javascript in our console.
  • Grunt task-runner for automation.
  • Mocha for our testing framework.
  • Chai for our assertion library.

The only thing you need to do before getting started is install Node.

Download and Install Node

If you don't know what the other tools are, don't worry. You can follow along step by step and learn the details later.

The Problem:

Create a Linked List data structure.

A linked list is just a list of 'nodes' in which each node has a reference to the next node in the list. The last node's 'next' reference is null.

The list itself has a reference to the 'head' (the first node) and the 'tail' (the last node).

It also has some functions to add and remove elements from the list, and check to see if a value is in the list.

Check out the Wikipedia page for a more detailed description.

To sum things up, a linked list is just a Javascript object with the following properties.

It should:

  • Have a head and a tail property.
  • Have an addToTail function that adds values to the list at the tail.
  • Have a removeFromHead function that removes the 'head' element from the list and returns the head's value.
  • Have a contains function that returns true or false representing whether or not a value is in the Linked List.

Installing necessary NPM modules

Node comes with a package manager, NPM. We will use NPM to install the command line tools and javascript dependencies that we need to automate and run our tests.

What is NPM?

Once you have node/npm installed, run the command

npm install -g grunt-cli mocha

This will install the grunt and mocha command-line interfaces so we can use the grunt and mocha commands from our terminal.

Setting up the testing foundation.

Start off installing some npm dependencies in a fresh directory.

You can create a package.json file and run npm install:

touch package.json

{
  "name": "Test Skeleton",
  "version": "1.0",
  "author": {
    "name": "Eric Ihli"
  },
  "devDependencies": {
    "chai": "^2.3.0",
    "grunt": "latest",
    "grunt-contrib-jshint": "^0.11.2",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-mocha-test": "^0.12.7",
    "mocha": "^2.2.4"
  }
}

Or you can install the dependencies individually with:

npm install grunt mocha chai grunt-contrib-watch grunt-mocha-test grunt-contrib-jshint

Create a basic Gruntfile with the following tasks.

touch Gruntfile.js

module.exports = function (grunt) {

  grunt.initConfig({

    pkg: grunt.file.readJSON('package.json'),

    jshint: {
      // This is the list of files on which grunt will run JSHint
      all: ['Gruntfile.js', 'package.json', '*.js'],
      options: {
        curly: true, // Always require {} blocks for if/while statements
        eqeqeq: true // Always require triple equals ===
      }
    },

    watch: {
      // These are the files that grunt will watch for changes.
      files: ['Gruntfile.js', 'package.json', '*.js'],
      // These are the tasks that are run on each of the above files every time there is a change.
      tasks: ['jshint', 'mochaTest'],
      options: {
        atBegin: true
      }
    },

    mochaTest: {
      src: ['tests.js']
    }
  });

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-mocha-test');

};

Save that file as a template to use in other projects. It is a solid foundation.

The command grunt watch can now be used to start watching, linting, and testing your code. Run that command now to make sure the Gruntfile.js is linting correctly.

Writing our first test

Test Driven Development involves writing tests before writing any code.

Once we have a test that describes how we want our code to work and we see it failing, we write small chunks of code that progress the test towards passing.

Following that practice, the first thing we need to do now that we have our Gruntfile and directory structure set up is to write a failing test.

Create the file tests.js (That's the file we told Grunt to watch for and run tests on every time we save a file.)

var should = require('chai').should();
var toyProblemModule = require('./problem.js');

describe("Linked List", function() {
  it('should initialize as an empty list with null head and tail', function() {
    var list = new toyProblemModule();
    should.equal(list.tail, null);
    should.equal(list.head, null);
  });
});

Our code will be inside toyProblem.js.

Right now inside of our terminal where we ran grunt watch we should see the following:

Image of console

Let's work through the errors one at a time. We'll start by creating the toyProblems.js file.

touch toyProblem.js

Image of console

Great! Progress! It doesn't look like much, but now we know our Gruntfile is working, it is properly watching our test files, it is properly running our Mocha tests, and it is properly asserting with Chai.

That is a lot of mental debt that we just paid off. Now we can focus on the problem itself and be confident that our code will be automatically tested in real time as we write it.

Passing our first test

Let's work on getting that first test passing.

We see that the error is:

TypeError: object is not a function at Context.<anonymous>(tests.js:6:16)

Line 6 of tests.js is: var list = new toyProblemModule();

In line 2 of our tests.js file, we set toyProblemModule equal to what we are importing from toyProblem.js.

In the spirit of Test Driven Development, we are going to make the smallest possible change that will make progress on a failing test.

Let's go to toyProblem.js and add a module.exports line to export a function.

What is module.exports in Node.js?

Add the following code to toyProblems.js:

module.exports = function() {

};

Your terminal has Grunt automatically watching the toyProblem.js file for changes so it will automatically re-run the test and you should get a new error as shown below.

Image of console

Now we see that on line 7, list.tail, which we expected to equal null, is actually equal to undefined.

Again, with TDD practices, we'll make the smallest change to make the code pass.

Let's update toyProblem.js so that the function we are exporting has a this.tail property equal to null.

module.exports = function() {
  this.tail = null;  
};

Save the file, look at the console to see that the test was automatically run, and you'll see that we are now getting the same error, but on line 8.

Image of console

Add another line of code to set this.head = null:

module.exports = function() {
  this.tail = null;
  this.head = null;
};

Save the file again and you should now have your first passing test!

Image of console

Writing the rest of our tests

Now that we know we have all of the bones of our test skeleton properly connected, we can write tests that describe all the functionality of our Linked List.

var should = require('chai').should();
var toyProblemModule = require('./toyProblem.js');

describe("Linked List", function() {

  it('should initialize as an empty list with null head and tail', function() {
    var list = new toyProblemModule();
    should.equal(list.tail, null);
    should.equal(list.head, null);
  });

  it.skip('head and tail should equal first item added', function() {
  });

  it.skip('head and tail should be correct when two items added', function() {
  });

  it.skip('should correctly removehead and update head and tail', function() {
  });

  it.skip('should return null and not break when removing from empty list', function() {
  });

  it.skip('should return true or false correctly when calling contains', function() {
  });

});

Here, we use Mocha's it.skip() command to mark a test as pending. It will show up in the console like this:

Image of pending tests

Now we can tackle these tests one at a time.

Let's remove the .skip from the first test and add some assertions.

it('head and tail should equal first item added', function() {
  var list = new toyProblemModule();
  list.add(5);
  list.head.value.should.equal(5);
  should.equal(list.head.next, null);
  list.tail.value.should.equal(5);
  should.equal(list.tail.next, null);
});

The first error we get is that our list object has no method 'add'.

Image of failing test

And once more... TDD... make the smallest change to progress the test.

Set this.add on our LinkedList to equal a function and watch the test progress.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function() {
  };
  
};

And our new error is that we cannot read property 'value' of null at line 15.

Image of failing test

If we look at line 15 of tests.js, we see list.head.value.should.equal(5);

So we are trying to read the property 'value' of 'list.head'. For 'list.head' to have a property 'value', we need to make 'list.head' an object so we can assign it properties.

Let's do that and save and see what happens.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function() {
    this.head = {value: 5};
  };
};

And our console shows:

Image of console

Bingo! Progress. That assertion is passing. Our new error message says that something is undefined when we want it to equal null.

We have a look at line 16 of tests.js should.equal(list.head.next, null);. We can see we are trying to read the 'next' property of 'list.head'. Right now it doesn't exist, so let's create it and set it equal to null.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function(val) {
    this.head = {
      value: val,
      next: null
    };
  };
};

Image of console

Once again, progress. Our new error is on line 17.

Keep going through each error, line-by-line changing the bare minimum bit of code to make progress on the tests.

The final code to get all of the assertions in our second test passing is:

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function(val) {
    this.head = {
      value: val,
      next: null
    };
    this.tail = {
      value: val,
      next: null
    };
  };
};

Image of passing test

Now that we have that test passing, we can remove 'skip' from the next test and add some assertions.

it('head and tail should be correct when two items added', function() {
  var list = new toyProblemModule();
  list.add(5);
  list.add(8);
  list.head.value.should.equal(5);
  list.head.next.value.should.equal(8);
  list.tail.value.should.equal(8);
  list.tail.next.should.equal(null);
});

And we can look at our console to see what we need to start work on.

Image of new failing test

And if we look at line 25 of tests.js we see we are expecting 'list.head.value' to equal '5' but instead it equals '8'.

Looking at our code in toyProblem.js we can see that the problem is that we are overwriting 'this.head' every time we call 'this.add'.

Instead, what we need to do is only set 'this.head' on the first call to 'this.add'. If we already have 'this.head' set, we want to leave it alone.

That seems to be the smallest bit of code we can add to advance the test.

Let's add that if statement around our 'this.head' assignment.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function(val) {
    if (this.head === null) {
      this.head = {
        value: val,
        next: null
      };      
    }
    this.tail = {
      value: val,
      next: null
    };
  };
};

And once again we see that our tests have progressed. We now have an error on line 26 of test.js.

Image of console

Line 26 of tests.js is list.head.next.value.should.equal(8);. So we are trying to read the property 'value' of 'this.head.next' but our tests say 'this.head.next' is null.

Let's fix that.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function(val) {
    if (this.head === null) {
      this.head = {
        value: val,
        next: null
      };      
    }

    this.head.next = {
      value: val
    };
    
    this.tail = {
      value: val,
      next: null
    };
  };
};

And now we take a look at our console and...

Image of console with new broken test

Our previous test that was passing is now broken!

We made progress on our current problem, but we broke something we did earlier. This is one of the powerful things of Test Driven Development. If we were just using console.logs to check things we were currently working on, we won't always know if something we changed broke something that we tested earlier.

This new error from our old test points to line 16 of tests.js which saying that 'list.head.next' should equal null, but instead it is equal to the object '{value: 5}'.

Because we are making such small incremental changes, it is easy to see our mistake.

We simply need to wrap the code we just added in an 'else' statement.

module.exports = function() {
  this.tail = null;
  this.head = null;

  this.add = function(val) {
    if (this.head === null) {
      this.head = {
        value: val,
        next: null
      };      
    } else {
      this.head.next = {
        value: val
      };      
    }
    this.tail = {
      value: val,
      next: null
    };
  };
};

And looking back at our console:

Image of console

Great! Our old test is passing again and we see progress on our current test.

Run with it!

I'll leave the rest of the tests and code as an exercise for you.

Keep making the smallest change you can that will make progress on your test.

If you are forced to change more than just a few lines of code to get your tests to pass, consider writing smaller more modular tests.

There is an initial learning curve with Test Driven Development during which it will seem like you're writing code much slower, but once you get over that learning curve, the time you save from having clean code and tests that help you spot bugs immediately when they are easy to track down will pay for itself 10 times over.