Test Driven Development (TDD/BDD)

Why do we test?

  • Stops errors entering the system
  • Reduces debug time to practically zero
  • Encourages simple clear coding

Example

Brief: Show the customers broadband packages

// JSON response from the server

[{
  "id": 827349,
  "name": {
    "first": "John",
    "last": "Smith"
  },
  "packages": ["sports", "nature", "travel"]
}, {
 ...
}]
<div>
    <h1>My Broadband Packages</h1>
    <ul>
        <li>Nature</li>
        <li>Travel</li>
        <li>Sports</li>
    </ul>
</div>

Then...

A few weeks later and the team want to build a new page showing a customer's packages; the prices of those packages, and; upgrade options

// JSON response from the server

[{
  "id": 827349,
  "name": {
    "first": "John",
    "last": "Smith"
  },
  "packages": [{
    "name": "sports",
    "cost": 50,
    "upgradeOptions": ["sportsPlus"]
  }, {
    ...
  }]
}, {
 ...
}]
<div>
    <h1>My Broadband Packages</h1>
    <ul>
        <li>Sports
            <span>Cost: £50</span>
            <a href="/upgrade?options="sportsplus">
            See upgrade options</a>
        </li>
        etc...
    </ul>
</div>

Meanwhile, on your old page...

<div>
    <h1>My Broadband Packages</h1>
    <ul>
        <li></li>
        <li></li>
        <li></li>
    </ul>
</div>

Uncaught TypeError: package.toLowerCase
is not a function(…)

#notBetter

So, what happened and what can we do about it?

In our case, the API was upgraded to meet the needs of another page. This changed the nature of the data we received and hence broke our program.

They can't be expected to check every page manually - some sites can have hundreds of pages potentially.

 

How do we solve this problem??

UNIT TESTING!!

  • You take each module of your codebase in isolation
  • You develop by breaking the module's process down into steps and setting 'expectations' to see that those steps work as expected.
  • As you develop further you'll develop a whole 'suite' of tests which can then be run by servers (Continuous Integration [CI] Servers)
  • If, when you/your coders commit code, their work fails any of their tests, or causes in the failure of any other tests (that other devs have written for their components/pages), then the code is rejected.
  • Emails, bug tracker notifications, code quality tools, etc. can all be triggered to then find out why...

How does it work?

Let's say we're building a game of tictactoe: What would I need?

Let's try some!!

  1. Your html page
  2. Stylesheet
  3. Vendor scripts (jQuery, etc)
  4. Your script
  5. A test runner page (optional)
  6. Testing Framework (Jasmine)
  7. A test specification (spec) file

index.html

This page has your working product on. It has your styles, your vendor scripts and your main script file.

 

It outputs your finished product.

specRunner.html

This page has the same, but in addition it has the jasmine unit testing framework and your specs.

 

It outputs the results of your unit tests.

main.js

tictactoeSpec.js

(function(){
  "use strict";


    //Application code goes here    


}());
(function() {
  "use strict";
  
describe("TicTacToe", function() {

    //test code goes here

});
}());

Always start with a failing test...

(function() {
  "use strict";
  
describe("TicTacToe", function() {
 

  it("It should be loaded into the browser and be accessible", function() {
    expect(window.TicTacToe).toBeDefined();
  });



});
}());

If we look at our specRunner page

So now we update our main.js to load the application

(function() {
  
    window.TicTacToe = function(sideLength, playerNames, options){
        
    };

}());

If we look at our specRunner page

Making life easy

(function() {
  "use strict";

describe("TicTacToe", function() {
  var game, board, squares, sideLength, playerNames;

  beforeEach(function() {
    sideLength = 3;
  });

  afterEach(function() {
    sideLength = null;
  });

  it("It should be loaded into the browser and be accessible", function() {
    expect(window.TicTacToe).toBeDefined();
  });

  describe('The Board', function() {
    it("It should create a board", function() {
      expect(board).not.toBeUndefined();
    });
  
  });

});
}());
  • We can carry out common tasks, such as initiating the application in a 'beforeEach' block.
  • We must also remember reset our code after each test in an 'afterEach' block.

Create the board

(function() {
  
    describe("TicTacToe", function() {
      var game, board;

      beforeEach(function() {
        game = new TicTacToe();
        game.init();
        board = document.getElementsByClassName('board')[0];
      });
     
      it("It should be loaded into the browser and be accessible", function() {
        expect(window.TicTacToe).toBeDefined();
      });
    
      describe("The Board", function() {
        it("It should create a board", function() {
          expect(board).not.toBeUndefined();
        });
      });

    });

}());

(again the test must fail first)

Board not present

Update main.js

(function() {
  "use strict";
  

 window.TicTacToe = window.TicTacToe ||

  function(sideLength, playerNames, options){
  //THE BOARD
    var boardSize, players = [];

    sideLength = sideLength || 3;
    boardSize = sideLength * sideLength;

    function createBoard(){
      //Create the board
      var board, squares;

      board = document.createElement("DIV");
      board.className = "board row";
      //etc.
  };

//etc.

}());

Board is now present...

Standby for RUBBISH code!!

(function() {
  "use strict";
  

 window.TicTacToe = window.TicTacToe ||

  function(sideLength, playerNames, options){
    
    function createBoard(){
      //Create the board
      var board, squares;

      board = document.createElement("DIV");
      board.className = "board row";
      document.body.appendChild(board);

      (function mySpecialContractorFunction(){
        window.TicTacToe = "I\'m well good, me!!!";
      }());

  };


}());

OH NO YOU DON'T!!

What can we test?

  • The values of variables
  • What functions were called and with what arguments
  • What they returned

 

Full documentation is available at:
http://jasmine.github.io/edge/introduction.html

Matchers (matching a value)

 describe("TicTacToe", function() {
  it("It should be loaded into the browser and be accessible", function() {
    expect(window.TicTacToe).toBeDefined();
    expect(window.hamster).not.toBe(7);
    expect(message).toMatch(/bar/);
    expect(foo).toBeTruthy();
  });
});

Spies, stubs & mocks

(function() {
  "use strict";
 
describe("A spy", function() {
  var foo, bar = null;

  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      }
    };

    spyOn(foo, 'setBar');

    foo.setBar(123);
    foo.setBar(456, 'another param');
  });

  it("tracks that the spy was called", function() {
    expect(foo.setBar).toHaveBeenCalled();
  });

  it("tracks all the arguments of its calls", function() {
    expect(foo.setBar).toHaveBeenCalledWith(123);
    expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
  });

});
}());

What frameworks are there?

Jest, Mocha, Sinon, Chai, QUnit, JUnit, Buster.js, etc. etc.
 

I personally use the first 4, but Buster is good and QUnit is an old favourite.

CI Services

Most CI services will run your tests for you:

Example services include:

  • CircleCI
  • Travis
  • CodeShip

Any Questions?

Test Driven Development (TDD)

By James Sherry

Test Driven Development (TDD)

  • 1,287