Test Driven Development Part II


[written by Roman Kollatschny and Matthias Schmidt]

Welcome back to the second article in our Node.js development series. Today, we are going to adapt the TDD cycle in an helloWorld example application. If you missed our first article about the principles of TDD, you can find it here.

In the last article, we learnd about the fundamentals of the test driven development process. That involved the five steps of the TDD cycle, which have to be repeated until the completion of the application. We also had a look at our two frameworks Mocha and Chai that we are using in this tutorial.

Let’s code!

TDD example coding

In our tutorial, we want to implement a simple and well known hello World application. Further we want to extend the funcitonality to greet you or others by their name. Before starting with our tutorial, we have to do some initial stuff.

Create our application structure

Prerequirements: We need a working Node.js runtime on our system.
For the structure of the example application it’s necessary to create some files and folders and then install the required Node-modules. There are a few steps to follow:

  • creating a folder node-tdd-tutorial and switching into it
  • executing npm init in the console to create a package.json file
    • we can leave all values on default by simply pressing enter
  • installing mocha globally by executing npm install -g mocha
  • installing chai locally with npm install -save chai
  • creating two folders app and test
  • creating two empty files app/app.js and test/test.js inside the just created folders

As result we should get the following structure.

node-tdd-tutorial:
│   package.json
│   
├───app
│       app.js
│       
├───node_modules
│       ...
│                       
└───test
        test.js

Now our application structure is ready and we can start coding. (Take a look at the current state by viewing the Github commit)

First iteration

For every requirement of our application, we have to walk through an interation of the TDD cycle (you may remember from our first blog post).
In the first iteration, we want to implement the requirement, that our application simply returns the string ‘Hello World’.

Writing our first test (TDD cylce #1)

In our first test case we test if our application returns a string which is equal to ‘Hello World’.

'use strict';

let expect = require('chai').expect;
let App = require('./../app/app.js');

describe('helloWorld app testsuite', function (); {

  it('returns the string "Hello World"', function (); {

    expect(App)
      .to.be.a('string')
      .and.is.equal('Hello World');

  });

});

The first step in the first iteration of the TDD cycle is completed. (Github commit)

Run our first test (TDD Cycle #2)

Now we run the test by executing the command mocha. The test fails as it should and we can tick the second step in the TDD cycle.

Failing first test
Failing the first test

Writing code so the test succeeds (TDD Cycle #3 – #5)

To complete the third cycle step we have to implement our code to fulfill our requirements and pass the failed test.

It’s time to edit our file app/app.js. We use the simpliest way to pass the test. This is by assigning the expected string to the module.exports-object. (Github commit)

'use strict';

module.exports = 'Hello World';

If we now run the test again it should pass. And step four is done. It’s significant to mind the importance of the steps two to four in the exactly order.

Passing the first test
Passing the first test

At this point we could refactor our code to keep it clean and optimize it. But because we have only a few lines of code there is no need of refactoring and we can skip it this time. Step five, check.

Second Iteration

In the second iteration we want to fulfill another requirement.

We have to repeat the steps one to five until all application requirements are met. In the following steps we will implement the real application instead of a single string return.

Writing more tests to expand functionality

Our application returns ‘Hello World’ and needs to be extended with the possibility to greet a user by its name.

We define a second test for the requirement that our function returns ‘Hello John’.

it('returns the string "Hello John"', function() {

    expect(App)
      .to.be.a('string')
      .and.is.equal('Hello John');

});

(Github commit)

As expected according to the development cycle our test fails because the returned string didn’t match our expected string ‘Hello John’.

Failing the Tests
Failing the tests

Code again

To fix this we implement a class with two functions that return either ‘Hello World’ or ‘Hello John’.

class Hello {

  helloWorld() {
    return 'Hello World';
  }

  helloJohn() {
    return 'Hello John';
  }
}

module.exports = Hello;

The test fails because we have implemented our class but our test cases still expects a single string. We have to update our test case to execute the proper class functions.

let hello = new App();

describe('helloWorld app testsuite', function() {

  it('returns the string "Hello World"', function() {

    expect(hello.helloWorld())
      .to.be.a('string')
      .and.is.equal('Hello World');

 });

  it('returns the string "Hello John"', function() {

    expect(hello.helloJohn())
      .to.be.a('string')
      .and.is.equal('Hello John');

  });

});

If we execut mocha our test passes again. (Github commit)

Now we are able to refactor our code to clean and optimize it.

Refactor

To remove unnecessary code and keep it clean we try to combine functions if possible. So we removed the helloJohn and helloWorld functions and added the needed functionality to cover the requirements. Instead of separete functions we only have one left (Github commit).

As result we have an application that is able to greet the world or a person by its name. It meets the previous defined requirements and therefore has gone two times through the TDD cycle. Since we have hard coded the returning name it’s necessary to do some more work to finish.
Step five, check.

Last Iteration

Last but not least we have our third iteration of the TDD cycle.

Adding the third test and code until it passes

The last requirement is the possibility to define the returning name. Therefore we have to add one more test case.

it('returns the string "Hello Joe" where Joe is passed as a variable', function() {

  expect(hello.hello('Joe'))
    .to.be.a('string')
    .and.is.equal('Hello Joe');

});

As we run the test it fails first. Then we have to extend our application code until the test passes. Now the code should cover all defined requirements and our helloWorld tutorial application is completed.

Congratulations! You may have finished your first application with test driven development! You can take a look at the finished tutorial code on Github.

What are your thoughts on this approach? Will you use TDD again? You may put your thoughts into the comments.