Front End Unit Testing

Technology Previews and Proposals

Objective

To examine unit testing options, discuss their pros and cons, and work towards a decision on which tool to use.

What is Unit Testing?

  • A software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation.

  • Small, focused tests on isolated functions or functionality

  • Meant to be fast and help isolate bugs

Tools under investigation

  • Jasmine
  • Mocha/Chai/Sinon
  • Tape
  • QUnit
  • Intern
  • BDD Style Syntax
  • Created in 2009
  • Actively maintained, sponsored by Pivotal Labs
  • 9,755 Stargazers, 1,571 forks on GitHub
  • Integrates with both Gulp and Grunt

Jasmine

describe("Player", function() {
  var player;
  var song;

  beforeEach(function() {
    player = new Player();
    song = new Song();
  });

  it("should be able to play a Song", function() {
    player.play(song);
    expect(player.currentlyPlayingSong).toEqual(song);

    //demonstrates use of custom matcher
    expect(player).toBePlaying(song);
  });

  //...

});
beforeEach(function() {
  this.addMatchers({
    toBePlaying: function(expectedSong) {
      var player = this.actual;
      return player.currentlyPlayingSong === expectedSong && 
             player.isPlaying;
    }
  });
});

spec-helper.js

player.spec.js

Jasmine

describe("Player", function() {
  var player;
  var song;

  // ...

  describe("when song has been paused", function() {
    beforeEach(function() {
      player.play(song);
      player.pause();
    });

    it("should indicate that the song is currently paused", function() {
      expect(player.isPlaying).toBeFalsy();

      // demonstrates use of 'not' with a custom matcher
      expect(player).not.toBePlaying(song);
    });

    it("should be possible to resume", function() {
      player.resume();
      expect(player.isPlaying).toBeTruthy();
      expect(player.currentlyPlayingSong).toEqual(song);
    });
  });

  // ...

});

Jasmine

describe("Player", function() {
  var player;
  var song;

  // ...

  // demonstrates use of spies to intercept and test method calls
  it("tells the current song if the user has made it a favorite", function() {
    spyOn(song, 'persistFavoriteStatus');

    player.play(song);
    player.makeFavorite();

    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
  });

  //demonstrates use of expected exceptions
  describe("#resume", function() {
    it("should throw an exception if song is already playing", function() {
      player.play(song);

      expect(function() {
        player.resume();
      }).toThrow("song is already playing");
    });
  });
});

Demo

Jest

  • Built on top of Jasmine
  • Automatically mocks dependencies
  • Uses jsdom to execute tests
  • Has multiple interfaces that include BDD, TDD, and QUnit styles
  • Created in 2011
  • Actively maintained
  • 7,710 Stargazers, 1,170 forks on GitHub
  • Integrates with both Gulp and Grunt
  • Automatic integration with promises
  • Can programmatically generate tests
  • Multiple reporters
  • Only has basic asserts built in, extra assertions often included by using Chai.
  • Lacks built in support for spies, Sinon is generally used to achieve this functionality.  

Chai jQuery assertions

$('#header').should.have.attr('foo');
expect($('body')).to.have.attr('foo', 'bar');
expect($('body')).to.have.attr('foo').match(/bar/);
$('#header').should.have.prop('disabled');
expect($('body')).to.have.prop('disabled', false);
expect($('body')).to.have.prop('value').match(/bar/);

Sinon Ajax Testing

function getTodos(listId, callback) {
    jQuery.ajax({
        url: "/todo/" + listId + "/items",
        success: function (data) {
            // Node-style CPS: callback(err, data)
            callback(null, data);
        }
    });
}

after(function () {
    // When the test either fails or passes, restore the original
    // jQuery ajax function (Sinon.JS also provides tools to help
    // test frameworks automate clean-up like this)
    jQuery.ajax.restore();
});

it("makes a GET request for todo items", function () {
    sinon.stub(jQuery, "ajax");
    getTodos(42, sinon.spy());

    assert(jQuery.ajax.calledWithMatch({ url: "/todo/42/items" }));
});

Demo

  • Simple test style that doesn't pollute global scope
  • Very minimalistic
  • Created in 2012
  • Maintenance has slowed in recent years
  • 1,403 Stargazers, 95 forks on GitHub
  • Integrates with both Gulp and Grunt
  • Outputs results in TAP (Test-Anything-Protocol)
  • Written by substack (author of browserify)

Example

var test = require('tape');

test('timing test', function (t) {
    t.plan(2);

    t.equal(typeof Date.now, 'function');
    var start = Date.now();

    setTimeout(function () {
        t.equal(Date.now() - start, 100);
    }, 100);
});
$ node example/timing.js
TAP version 13
# timing test
ok 1 should be equal
not ok 2 should be equal
  ---
    operator: equal
    expected: 100
    actual:   107
  ...

1..2
# tests 2
# pass  1
# fail  1

Tap Output

  • Multiple different interfaces include object, TDD, BDD, and QUnit.
  • Created in 2012
  • Actively maintained
  • 3,064 Stargazers, 228 forks on GitHub
  • Integrates with both Gulp and Grunt
  • AMD by default (thus integrates well with require.js)
  • Built in support for source maps and code coverage
  • Supported by the Dojo foundation

Object Interface

define(function (require) {
  var tdd = require('intern!tdd');

  tdd.suite('Suite name', function () {
    tdd.before(function () {
      // executes before suite starts
    });

    tdd.after(function () {
      // executes after suite ends
    });

    tdd.beforeEach(function () {
      // executes before each test
    });

    tdd.afterEach(function () {
      // executes after each test
    });

    tdd.test('Test foo', function () {
      // a test case
    });

  });

ES6 Object Interface

TDD Inteface

define(function (require) {
  var tdd = require('intern!tdd');

  tdd.suite('Suite name', function () {
    tdd.before(function () {
      // executes before suite starts
    });

    tdd.after(function () {
      // executes after suite ends
    });

    tdd.beforeEach(function () {
      // executes before each test
    });

    tdd.afterEach(function () {
      // executes after each test
    });

    tdd.test('Test foo', function () {
      // a test case
    });

  });

BDD Inteface

define(function (require) {
  var bdd = require('intern!bdd');

  bdd.describe('the thing being tested', function () {
    bdd.before(function () {
      // executes before suite starts
    });

    bdd.after(function () {
      // executes after suite ends
    });

    bdd.beforeEach(function () {
      // executes before each test
    });

    bdd.afterEach(function () {
      // executes after each test
    });

    bdd.it('should do foo', function () {
      // a test case
    });

  });
});

QUnit

  • Created in 2008
  • Very Actively maintained by the JQuery team
  • 3,293 Stargazers, 675 forks on GitHub
  • Integrates with both Gulp and Grunt

Questions?

Front End Unit Testing

By Justin Bennett

Front End Unit Testing

  • 702