Test Driven Development, Part 3

JavaScript testing

This is the third part in a short series on applying test driven development to a JavaScript program using Mocha, Chai, and Sinon. Part 1 and Part 2 gave a general overview of TDD and a brief walk-through of how to set up a testing environment. Part 3 will give a step-by-step example of the TDD process. I strongly recommend following along with your own code if you have yet to implement TDD at on your own, so if you have not set up your environment using the process described in Part 2, I suggest doing so before reading this post.

The Spec Code and Rendering

If you’ve been following along, you know that we’re designing a very simple program to identify if an array of numbers contains an arithmetic or geometric sequence. We have yet to write any code in our function, but we do have a pre-populated html file linked to both our code, and our testing spec; this will allow us to run our tests in the browser. Here is a comparison of the spec file and the rendered html:

Spec to browser comparison

Again, the describe and it statements are from the Mocha framework, while the expect statements are from Chai. Notice how these are rendered to the browser:

  • The text inside of the describe statement is rendered as a heading for a set of tests.
  • Each it statement is rendered as a different test; if the test is passing, we see a green check mark, while if it is failing, we see a red “x”
  • Finally, Chai provides additional information for any failing test in the form of an error.

Clicking on each of the tests will reveal the specific expect statement to which they relate; this can be quite helpful when trying to interpret the results. Of the three default tests provided by the test generator we are using, only the first provides us with any real information—that our function exists. The other two are just placeholders to get us started writing our own tests.

Creating Objectives

Before writing the specific tests, let’s think about the overarching goal of our little program and then break it down into smaller objectives (for those of you that read Part 1, this is analogous to “unpacking” local, state and national standards).

We know we want our program to identify if a set of numbers is an arithmetic or geometric sequence. Let’s think more deeply about what that means our program will need to do:

  1. Accept an input of numbers in the form of an array or string
  2. Determine if the sequence of numbers has a common difference or common ratio
  3. Output the result as a string stating if the sequence was arithmetic, geometric, or neither.

Each of these objectives can then be translated into smaller tests that our program will either pass or fail. Because we will then working to pass just one test at a time, I feel it is best to start with the tests for the output, and then work our way backwards; this strategy will not work for all programs, but it does fit this situation nicely. Some of these tests may seem a bit contrived, but I think it is important to illustrate the fact that you want to write tests for the smallest bit of code you can test at a time.

We will first write these tests in pseudocode, and then translate them into Mocha and Chai.

The Output Message

//It should return the result as a string.
//If the sequence is arithmetic, it should return "This sequence is arithmetic"
//If the sequence is geometric, it should return "This sequence is geometric"
//Otherwise, it should return "This sequence is not a recognized pattern"

The Function

//It should identify an arithmetic sequence.
//It should identify a geometric sequence.
//It should not identify other types of sequences
//It should work for decreasing functions.

The Input Data

//It should accept an array of numbers
//It should accept a string of numbers that are comma delimited
//It should accept a string of numbers that are space delimited

Writing the Code

####Describe Statements Now that we know what behavior we expect our program to have, it is time to start writing the actual tests. Each section of tests lends itself nicely to a Mocha describe statement; let’s start by writing those.

//This first line was written by the test generator; we will keep it to 
//maintain the overarching focus on our sequenceType function.

describe('sequenceType()', function () {

  it('exists', function () {
    expect(sequenceType).to.be.a('function');
  });

  describe('The Output Message', function(){
  });

  describe('The Function Code', function(){
  });

  describe('The Input Message', function(){
  });

});

Notice each describe statement is a function that takes two parameters: a general title, and a callback function. Our individual tests will go inside of that callback function. For now, when we open this function in the browser, this is what we see:

Browser render of empty tests

The empty describe statements will not render until we give them some it statements.

If statements

describe('sequenceType()', function () {
  'use strict';

  it('exists', function () {
    expect(sequenceType).to.be.a('function');
  });

  describe('The Output Message', function(){
    it('should return the result as a string', function(){
    });

    it('should return "This sequence is arithmetic" for arithmetic sequences', function(){
    });

    it('should return "This sequence is geometric" for geometric sequences', function(){
    });

    it('should return "This sequence is not a recognized pattern" otherwise', function(){
    }); 

  });

  describe('The Function Code', function(){
    it('should identify an arithmetic sequence', function(){
    });

    it('should identify an geometric sequence', function(){
    });

    it('should not identify other sequences', function(){
    });

    it('should work for decreasing functions', function(){
    });

  describe('The Input Message', function(){
    it('should accept an array of numbers', function(){
    });

    it('should accept a string of numbers that are comma delimited', function(){
    });

    it('should accept a string of numbers that are space delimited', function(){
    });
  });

});

The browser rendering is now starting to look more like we want it to: Empty it statements; all passing

All of our tests are currently passing, because we haven’t actually written any assertions. If you want to focus on any individual test, you can do so by clicking on one of the small arrows to the right. Clicking on a description heading will also allow you to focus on one set of related tests.

Assertions

At this point, we are finally ready to write the tests themselves. Instead of writing them all at once, we’re just going to work on one section of tests at a time; this will keep our program organized and require us to maintain focus. We are going to write our tests using Chai’s chainable expect style. I suggest reading through their API to see all of the possible methods for constructing statements; for now, we will stick with a few basic patterns:

  • expect(something).to.be.a(something)
  • expect(something).to.equal(something)
describe('The Output Message', function(){
    
    it('should return the result as a string', function(){
      expect(returnValue).to.be.a('string');  
    });

    it('should return "This sequence is arithmetic" for arithmetic sequences', function(){
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should return "This sequence is geometric" for geometric sequences', function(){
      expect(returnValue).to.equal('This sequence is geometric');
    });

    it('should return "Not a recognized pattern" otherwise', function(){
      expect(returnValue).to.equal('Not a recognized pattern');
    }); 

  });

Now when we open up our spec, we see that we hae four failing tests. We can also see that the four failing tests all display the same message:

ReferenceError: returnValue is not defined

This is followed by information about where this error is occurring for each test. We care most about the top line of that location, which reads:

at Context.<anonymous> (file:///Users/haileyfoster/Desktop/Arith-Geo-Test/spec/sequenceType.js:14:14)

We can see that our error is occurring in our spec file, at line 14, character 14. Looking at our spec, we can see this is because we are calling on a variable, returnValue that we haven’t actually defined anywhere in our test. Let’s go ahead and define this variable in the spec above all of the itstatements in this section.

describe('The Output Message', function(){
    var returnValue; //defining the variable here will give the following tests access to the variable
    
    it('should return the result as a string', function(){  
      expect(returnValue).to.be.a('string');  
    });

Now our tests are giving us an assertion error:

AssertionError: expected undefined to be a string

Passing the tests

Finally, we are at the point where we can turn to our sequenceType function and write enough code there so that it passes one test at a time. Our first test tells us it should return a string. Let’s make this test pass with the smallest amount of code possible:

var sequenceType = function ( ) {
  return 'string';
};

Hmmm…if we run the spec, we see we’re getting the same error as before. The problem is that we haven’t actually called our function from the spec yet. Let’s do that:

it('should return the result as a string', function(){
    returnValue = sequenceType();
    expect(returnValue).to.be.a('string');   
});

Great! By giving our function a very simple return statement, our first test is passing.

From here, I’m just going to present the the work necessary to get each consecutive test to pass, only commenting when necessary to clarify the thinking process.

//Spec
it('should return "This sequence is arithmetic" for arithmetic sequences', function(){
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

//Passing Code
var sequenceType = function ( ) {
  return 'This sequence is arithmetic';
};
//Spec
it('should return "This sequence is geometric" for geometric sequences', function(){
      expect(returnValue).to.equal('This sequence is geometric');
    });

//Passing Code
var sequenceType = function ( ) {
  return 'This sequence is geometric';
};

Uh-oh! Now when we run our spec, we see that the third test passes, but the second test is now failing! Obviously this is because we were just changing the returned string. Time to rethink our code so that both tests will pass.

First we will need to update our code so that it will take a parameter and return a different message depending upon the argument passed in. We also need to update our spec file to pass different arguments to the function for each test.

//Our updated specs
    it('should return "This sequence is arithmetic" for arithmetic sequences', function(){
      returnValue = sequenceType("1,3,5,7");
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should return "This sequence is geometric" for geometric sequences', function(){
      returnValue = sequenceType("2,4,8,16");
      expect(returnValue).to.equal('This sequence is geometric');
    });

//The updated code
var sequenceType = function (sequence) {
  return sequence === "1,3,5,7" ? 'This sequence is arithmetic': 'This sequence is geometric';
};

Of course this is only going to work for these exact sequences, but right now, that doesn’t matter. We just want to get the specs to pass; we will use later tests to ensure that it works for any sequence passed it. Moving on…

//Spec
    it('should return "This sequence is arithmetic" for arithmetic sequences', function(){
      returnValue = sequenceType("1,3,5,7");
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should return "This sequence is geometric" for geometric sequences', function(){
      returnValue = sequenceType("2,4,8,16");
      expect(returnValue).to.equal('This sequence is geometric');
    });

    it('should return "Not a recognized pattern" otherwise', function(){
      returnValue = sequenceType("1,4,2,9");
      expect(returnValue).to.equal('Not a recognized pattern');
    }); 

//Code
var sequenceType = function (sequence) {
  if (sequence === "1,3,5,7"){
    return 'This sequence is arithmetic';
  } else if (sequence === "2,4,8,16"){
    return 'This sequence is geometric';
  } else {
    return 'Not a recognized pattern';
  }
};

On to the next set of tests! We’ll just basically rinse and repeat what we did above: Write failing tests, write code to pass the test, refactor. Because all of these tests are also going to use the variable returnValue, we will move this declaration to the outer describe function, so that all inner functions have access to it. We will also pass in our sequences as an array instead of a string so we have one less thing to deal with in our tests.

//The Spec
  describe('The Function Code', function(){
    
    it('should identify an arithmetic sequence.', function(){
      returnValue = sequenceType([1,3,5,7]);
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should identify a geometric sequence.', function(){
      returnValue = sequenceType([2,4,8,16]);
      expect(returnValue).to.equal('This sequence is geometric');
    });

    it('should not identify other sequences', function(){
      returnValue = sequenceType([1,4,2,9]);
      expect(returnValue).to.equal('Not a recognized pattern');
    });
    it('should work for decreasing functions', function(){
      var deArith = sequenceType([9,5,1,-3]);
      var deGeo = sequenceType([48,12,3,3/4]);
      expect(returnValue).to.equal('Not a recognized pattern');
    });
  });

  //The Code
  var arithTest;
  for (var i = 0; i < sequence.length-2; i++) {
    if (sequence[i+2]-sequence[i+1] === sequence[i+1]-sequence[i]){
      arithTest = true;
    } else{
      arithTest = false;
    }
    arithTest = arithTest === true? true: false;
  }
  if (arithTest){
    return 'This sequence is arithmetic';
  }

  var geoTest;
  for (var j = 0; j < sequence.length-2; j++) {
    if (sequence[j+2]/sequence[j+1] === sequence[j+1]/sequence[j]){
      geoTest = true;
    } else{
      geoTest = false;
    }
    geoTest = geoTest === true? true: false;
  }
  if (geoTest){
    return 'This sequence is geometric';
  }

  else {
    return 'Not a recognized pattern';
  }
};

We made our next test pass with this bit of code, but now our second test is failing. Failing test

Opening up the details of our spec, we see this is because this test is passing in a string, and our function can only work with arrays right now. Reason for failing test

The easiest way to fix this is to change our input for this test to an array instead of a string.

//The Spec
describe('The Output Message', function(){
    
    it('should return the result as a string', function(){
      returnValue = sequenceType([1,2,3,4]);
      expect(returnValue).to.be.a('string');
    
    });

We’ll need to remember to change the input for the other tests as well.

Moving on…

//The Spec
describe('The Function Code', function(){
    
    it('should identify an arithmetic sequence.', function(){
      returnValue = sequenceType([1,3,5,7]);
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should identify a geometric sequence.', function(){
      returnValue = sequenceType([2,4,8,16]);
      expect(returnValue).to.equal('This sequence is geometric');
    });

    it('should not identify other sequences', function(){
      returnValue = sequenceType([1,4,2,9]);
      expect(returnValue).to.equal('Not a recognized pattern');
    });
    it('should work for decreasing functions', function(){
      var deArith = sequenceType([9,5,1,-3]);
      var deGeo = sequenceType([48,12,3,3/4]);
      expect(returnValue).to.equal('Not a recognized pattern');
    });
  });

  //The Code
  var sequenceType = function (sequence) {
  if (typeof sequence === 'string'){
    sequence = sequence.split(',');
  }

  var arithTest;
  for (var i = 0; i < sequence.length-2; i++) {
    if (sequence[i+2]-sequence[i+1] === sequence[i+1]-sequence[i]){
      arithTest = true;
    } else{
      arithTest = false;
    }
    arithTest = arithTest === true? true: false;
  }
  if (arithTest){
    return 'This sequence is arithmetic';
  }

  var geoTest;
  for (var j = 0; j < sequence.length-2; j++) {
    if (sequence[j+2]/sequence[j+1] === sequence[j+1]/sequence[j]){
      geoTest = true;
    } else{
      geoTest = false;
    }
    geoTest = geoTest === true? true: false;
  }
  if (geoTest){
    return 'This sequence is geometric';
  }

  else {
    return 'Not a recognized pattern';
  }
};

Now for our final set of tests

//The Spec
describe('The Input Message', function(){
    it('should accept an array of numbers', function(){
      returnValue = sequenceType([5,16,27,38]);
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should accept a string of numbers that are comma delimited', function(){
      returnValue = sequenceType("5,16,27,38");
      expect(returnValue).to.equal('This sequence is arithmetic');
    });

    it('should accept a string of numbers that are space delimited', function(){
      returnValue = sequenceType("5 16 27 38");
      expect(returnValue).to.equal('This sequence is arithmetic');
    });
  });

Our function already passes the first test here, but we can see we need to add some code to deal with string inputs.

//The code
var sequenceType = function (sequence) {
  if (typeof sequence === 'string'){
    sequence = sequence.split(',');
  }

  var arithTest = true;
  for (var i = 0; i < sequence.length-2; i++) {
    if (sequence[i+2]-sequence[i+1] !== sequence[i+1]-sequence[i]){
      arithTest = false;
      }
      arithTest = arithTest === true? true: false;
  }
  if (arithTest){
    return 'This sequence is arithmetic';
  }

  var geoTest = true;
  for (var i = 0; i < sequence.length-2; i++) {
    if (sequence[i+2]/sequence[i+1] !== sequence[i+1]/sequence[i]){
      geoTest = false;
      }
      geoTest = geoTest === true? true: false;
  }
  if (geoTest){
    return 'This sequence is geometric';
  }

  else {
    return 'Not a recognized pattern';
  }
};

Almost there! Only the last test is failing; this is a pretty easy fix using a regex when we split the string.

//The Code
if (typeof sequence === 'string'){
    sequence = sequence.split(/[,\w/]/);
  }

And that’s it! Our function passes all of our specs and does exactly what we wanted it to do. We can now refactor our code a bit to make it cleaner and more efficient, checking our specs as we make changes to ensure they all still pass. Here is the final refactored code:

var sequenceType = function (sequence) {
  if (typeof sequence === 'string'){
    sequence = sequence.split(/[,\w/]/);
  }

  var arithTest, geoTest;
  var last, current, next;

  for (var i = 1; i < sequence.length-1; i++) {
    
    last = sequence[i-1];
    current = sequence[i];
    next = sequence[i+1];
    
    arithTest = next-current === current-last ? true : false;
    arithTest = arithTest === true? true: false;

    geoTest = next/current === current/last ? true: false;
    geoTest = geoTest === true? true: false;
  }

  if (arithTest){
    return 'This sequence is arithmetic';
  }else if (geoTest){
    return 'This sequence is geometric';
  } else {
    return 'Not a recognized pattern';
  }
};

In the final part of this series, we will look into the power Sinon can give to our tests.

Written on July 17, 2015