Unit testing JavaScript using Mocha and Node.js
Josh Mock Senior JavaScript architect at Emma Twitter: Email: @JoshMock josh@joshmock.com
What is unit testing? Write code to test code Ensures code works as expected Granular, single-focus assertions Not a substitute for QA
Why unit test? Confidence Easier refactoring Less regression Less complexity TDD is fun!
What is Node.js?
Install Node.js node.js.org/download/ OS X (with Homebrew installed): brew install node
What is Mocha?
Install Mocha npm install -g mocha
Test some code! var Car = function () { this.make = "Honda"; this.model = "Civic"; };
var assert = require("assert"); describe("Car", function () { describe("constructor", function () { it("should default the car to be a Honda Civic"); }); describe("makeAndModel", function () { it("should return a string containing the make and model"); }); });
Run, tests, run mocha path/to/test/file.js
How to write good tests Test results, not internals One focus per test Testing DOM changes is bold
How to write testable code
Simple, single-purpose functions // bad var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); this.list.sort(); } }; // good var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); }, sort: function () { this.list.sort(); } };
Avoid tight coupling of components var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); } }; // bad var math = { add: function () { var total = 0; for (var i = 0; i < numbers.list.length; i++) { total += numbers.list[i]; } return total; }, average: function () { return this.add() / numbers.list.length; } }; alert(math.average()); // good var math = { add: function (numList) { var total = 0; for (var i = 0; i < numList.length; i++) { total += numList[i]; } return total;
Separate business logic from UI (and avoid anonymous functions/callbacks) var numbers = [2, 4, 1, 3, 5]; // bad $("a.sort-numbers").on("click", function (e) { e.preventDefault(); numbers.sort(); }); // good var sortNumbers = function (e) { e && e.preventDefault && e.preventDefault(); numbers.sort(); }; $("a.sort-numbers").on("click", sortNumbers);
Advanced stuff!
Asynchronous tests
var asyncSort = function (numbers, callback) { setTimeout(function () { callback(numbers.sort()); }, 10); };
define("asyncSort", function () { it("should sort my numbers", function (done) { asyncSort([1, 3, 2], function (result) { assert.deepEqual(result, [1, 2, 3]); done(); }); }); });
Sinon.js npm install -g sinon
Spies var sinon = require("sinon"); it("runs jQuery.ajax", function () { sinon.spy($, "ajax"); doAjaxCall(); assert($.ajax.calledOnce); $.ajax.restore(); }); it("does some thing that takes forever", function () { someGlobal.slowFunction = sinon.spy(); callSlowFunction(); assert.equal(someGlobal.slowFunction.callCount, 1); assert(someGlobal.slowFunction.calledWith(1, "two", 3)); });
Stubs var sinon = require("sinon"); it("returns the age of a person with data stored in the database", function () { Database.get = sinon.stub().returns({ name: "Joe", age: 33 }); var getAge = function () { return Database.get("Joe").age; }; assert.equals(getAge(), 33); });
Mocks var sinon = require("sinon"); it("should get the desired car from the database", function () { var mock = sinon.mock(Database); mock .expects("getCar") .withExactArgs("Honda Civic") .once() var car = new Car(); car.get("Honda Civic"); assert(mock.verify()); });
Fake timers var sinon = require("sinon"); it("should save after 30 seconds", function () { var clock = sinon.useFakeTimers(); sinon.spy($, "ajax"); delayedSave(); clock.tick(30001); assert($.fn.ajax.called); $.ajax.restore(); });
jsdom and node-jquery Test browser-dependent code Make Node think it's a browser Test jQuery DOM manipulations Go through all stages of grief getting it to work Ponder using a browser-based framework instead
Install npm install -g jsdom && npm install -g jquery
Set up GLOBAL.document = require("jsdom").jsdom(); GLOBAL.window = document.createWindow(); GLOBAL.$ = GLOBAL.jQuery = require("jquery").create(window);
Use it("should change div background color to blue", function () { $("body").html('<div id="mydiv"></div>'); $("#mydiv").css("background", "blue"); assert.equal($("#mydiv").css("background"), "blue"); });
No headless browser No GUI running in background No guarantees
THE END Questions? Twitter: Email: @JoshMock josh@joshmock.com github.com/JoshMock/mocha-node-slides

Unit testing JavaScript using Mocha and Node

  • 1.
  • 2.
    Josh Mock Senior JavaScriptarchitect at Emma Twitter: Email: @JoshMock josh@joshmock.com
  • 3.
    What is unittesting? Write code to test code Ensures code works as expected Granular, single-focus assertions Not a substitute for QA
  • 4.
    Why unit test? Confidence Easierrefactoring Less regression Less complexity TDD is fun!
  • 5.
  • 6.
    Install Node.js node.js.org/download/ OS X(with Homebrew installed): brew install node
  • 7.
  • 8.
  • 9.
    Test some code! varCar = function () { this.make = "Honda"; this.model = "Civic"; };
  • 10.
    var assert =require("assert"); describe("Car", function () { describe("constructor", function () { it("should default the car to be a Honda Civic"); }); describe("makeAndModel", function () { it("should return a string containing the make and model"); }); });
  • 11.
    Run, tests, run mochapath/to/test/file.js
  • 19.
    How to writegood tests Test results, not internals One focus per test Testing DOM changes is bold
  • 20.
    How to writetestable code
  • 21.
    Simple, single-purpose functions //bad var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); this.list.sort(); } }; // good var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); }, sort: function () { this.list.sort(); } };
  • 22.
    Avoid tight couplingof components var numbers = { list: [1, 2, 3], add: function (newNum) { this.list.push(newNum); } }; // bad var math = { add: function () { var total = 0; for (var i = 0; i < numbers.list.length; i++) { total += numbers.list[i]; } return total; }, average: function () { return this.add() / numbers.list.length; } }; alert(math.average()); // good var math = { add: function (numList) { var total = 0; for (var i = 0; i < numList.length; i++) { total += numList[i]; } return total;
  • 23.
    Separate business logicfrom UI (and avoid anonymous functions/callbacks) var numbers = [2, 4, 1, 3, 5]; // bad $("a.sort-numbers").on("click", function (e) { e.preventDefault(); numbers.sort(); }); // good var sortNumbers = function (e) { e && e.preventDefault && e.preventDefault(); numbers.sort(); }; $("a.sort-numbers").on("click", sortNumbers);
  • 24.
  • 25.
  • 26.
    var asyncSort =function (numbers, callback) { setTimeout(function () { callback(numbers.sort()); }, 10); };
  • 27.
    define("asyncSort", function (){ it("should sort my numbers", function (done) { asyncSort([1, 3, 2], function (result) { assert.deepEqual(result, [1, 2, 3]); done(); }); }); });
  • 28.
  • 29.
    Spies var sinon =require("sinon"); it("runs jQuery.ajax", function () { sinon.spy($, "ajax"); doAjaxCall(); assert($.ajax.calledOnce); $.ajax.restore(); }); it("does some thing that takes forever", function () { someGlobal.slowFunction = sinon.spy(); callSlowFunction(); assert.equal(someGlobal.slowFunction.callCount, 1); assert(someGlobal.slowFunction.calledWith(1, "two", 3)); });
  • 30.
    Stubs var sinon =require("sinon"); it("returns the age of a person with data stored in the database", function () { Database.get = sinon.stub().returns({ name: "Joe", age: 33 }); var getAge = function () { return Database.get("Joe").age; }; assert.equals(getAge(), 33); });
  • 31.
    Mocks var sinon =require("sinon"); it("should get the desired car from the database", function () { var mock = sinon.mock(Database); mock .expects("getCar") .withExactArgs("Honda Civic") .once() var car = new Car(); car.get("Honda Civic"); assert(mock.verify()); });
  • 32.
    Fake timers var sinon= require("sinon"); it("should save after 30 seconds", function () { var clock = sinon.useFakeTimers(); sinon.spy($, "ajax"); delayedSave(); clock.tick(30001); assert($.fn.ajax.called); $.ajax.restore(); });
  • 33.
    jsdom and node-jquery Testbrowser-dependent code Make Node think it's a browser Test jQuery DOM manipulations Go through all stages of grief getting it to work Ponder using a browser-based framework instead
  • 34.
    Install npm install -gjsdom && npm install -g jquery
  • 35.
    Set up GLOBAL.document =require("jsdom").jsdom(); GLOBAL.window = document.createWindow(); GLOBAL.$ = GLOBAL.jQuery = require("jquery").create(window);
  • 36.
    Use it("should change divbackground color to blue", function () { $("body").html('<div id="mydiv"></div>'); $("#mydiv").css("background", "blue"); assert.equal($("#mydiv").css("background"), "blue"); });
  • 37.
    No headless browser NoGUI running in background No guarantees
  • 38.