Test-driven JavaScript with

Testing, 1.. 2.. 3..

Mocha, Chai, and Sinon

Test Driven Development

The idea that you write tests first, then you write code.

As a result, you think about how you will use the code before you implement it.

assert(isEven(2), true)
function isEven(n) {
  return n % 2 === 0;
}

FAIL

PASS

Red, Green, Refactor

Write just enough code

Unit tests should be specific, and so should the code to make the test pass.

Once it passes, refactor, and move on to the next test.

Common pitfalls

  • Forgetting to run tests frequently
  • Writing too many tests at once
  • Writing tests that are too large or coarse-grained
  • Writing overly trivial tests, for instance omitting assertions
  • Writing tests for trivial code, for instance accessors

Mocha is a feature-rich JavaScript testing framework that runs on both the browser and Node.

var assert = require("assert");

describe('Array', function(){
  describe('#indexOf()', function(){
    it('should return -1 when the value is not present', function(){
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    })
  })
})

Mocha

Interfaces

Mocha offers developers different domain specific languages (DSLs) for writing tests.

  • BDD
  • TDD
  • Exports
  • QUnit
  • Require

Interfaces: BDD

describe('Array', function(){
  before(function(){
    // ...
  });

  describe('#indexOf()', function(){
    it('should return -1 when not present', function(){
      [1,2,3].indexOf(4).should.equal(-1);
    });
  });
});

Interfaces: TDD

suite('Array', function(){
  setup(function(){
    // ...
  });

  suite('#indexOf()', function(){
    test('should return -1 when not present', function(){
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

Interfaces: Exports

module.exports = {
  before: function(){
    // ...
  },

  'Array': {
    '#indexOf()': {
      'should return -1 when not present': function(){
        [1,2,3].indexOf(4).should.equal(-1);
      }
    }
  }
};

Interfaces: QUnit

function ok(expr, msg) {
  if (!expr) throw new Error(msg);
}

suite('Array');

test('#length', function(){
  var arr = [1,2,3];
  ok(arr.length == 3);
});

test('#indexOf()', function(){
  var arr = [1,2,3];
  ok(arr.indexOf(1) == 0);
  ok(arr.indexOf(2) == 1);
  ok(arr.indexOf(3) == 2);
});

suite('String');

test('#length', function(){
  ok('foo'.length == 3);
});

Interfaces: Require

var testCase = require('mocha').describe
var pre = require('mocha').before
var assertions = require('mocha').assertions
var assert = require('assert')

testCase('Array', function(){
  pre(function(){
    // ...
  });

  testCase('#indexOf()', function(){
    assertions('should return -1 when not present', function(){
      assert.equal([1,2,3].indexOf(4), -1);
    });
  });
});

Asynchronus support

Mocha makes it simple to test your asynchronous code using a done() callback function.

it('should save without error', function(done){
  var user = new User('Luna');
  user.save(function(err){
    if (err) throw err;
    done();
  });
})
it('should save without error', function(done){
  var user = new User('Luna');
  user.save(done);
})

To make things easier the done() callback accepts an error:

File watcher support

Automatically runs tests whenever a file is changed.

$ mocha -w

Test Coverage

Mocha keeps track of which lines of code have been tested, which other libraries can access.

An assertion library for node and the browser that can be paired with any JavaScript testing framework.

Chai

Provides many plugins for testing your code, including:

  • HTTP requests
  • Promises
  • Selenium
  • DOM assertions

Should interface

Extends each object with a should property to start your assertion chain.

var foo = 'bar',
    beverages = {tea: [ 'chai', 'matcha', 'oolong']};

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
tea.should.have.property('flavors').with.length(3);

Expect interface

Extends each object with a should property to start your assertion chain.

var foo = 'bar',
    beverages = {tea: [ 'chai', 'matcha', 'oolong']};

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(beverages).to.have.property('tea').with.length(3);
// AssertionError: topic [answer]: expected 43 to equal 42.
expect(answer, 'topic [answer]').to.equal(42);
var value = undefined;
expect(value).to.be.undefined;

Assert interface

classic assert-dot notation, similiar to that packaged with node.js. This assert module, however, provides several additional tests and is browser compatible.

var foo = 'bar',
    beverages = {tea: [ 'chai', 'matcha', 'oolong']};

assert.typeOf(foo, 'string', 'foo is a string');
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

Standalone test spies, stubs, and mocks for JavaScript.

Sinon

Provides

  • Spies

  • Stubs

  • Fake XML/HTTP Requests

  • and more

Spies

Allow you to check the conditions surrounding how a function was invoked.

it("calls the original function", function () {
    var callback = sinon.spy();
    var proxy = once(callback);

    proxy();

    assert(callback.called);
});

Stubs

Test stubs are functions (spies) with pre-programmed behavior.

it("returns the return value from the original function", function () {
    var returnsFourtyTwo = sinon.stub().returns(42);

    assert.equals(returnsFourtyTwo(), 42);
});

Testing Ajax

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" }));
});

Fake XMLHttpRequest

var xhr, requests;

before(function () {
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function (req) { requests.push(req); };
});

after(function () {
    // Like before we must clean up when tampering with globals.
    xhr.restore();
});

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

    assert.equals(requests.length, 1);
    assert.match(requests[0].url, "/todo/42/items");
});

Questions?

Example repo

Testing, 1.. 2.. 3...

By Tony Gaskell

Testing, 1.. 2.. 3...

Writing tests for JavaScript using Mocha, Chai, and Sinon

  • 1,649