Javascript Testing:
Units; Backbone Models and Views
or, How To Avoid Race Conditions and Live Longer
jQuery's Test Suite
- Used by the jQuery project to test its code and plugins
- Capable of testing any generic JavaScript code
- ...even server-side
- JQuery-dependent
test("a basic test example", function() {
ok( true, "this test is fine" );
var value = "hello";
equal( value, "hello", "We expect value to be hello" );
});
module("Module A");
test("first test within module", function() {
ok( true, "all pass" );
});
test("second test within module", function() {
ok( true, "all pass" );
});
Javascript BDD
- Framework independent
- Async specs
- Easily extensible matchers
- Simple spy support
- Runs client/server-side
- Ruby/Node/Java/Python/.NET integrations
describe("Jasmine", function() {
it("makes testing JavaScript awesome!", function() {
expect(yourCode).toBeLotsBetter();
});
it("uses natural language syntax", function() {
expect(true).toBeTruthy();
});
});
- Some failing

- All pass
Grandfather Time
- XUnit compatible (i.e. JUnit port)
- Client-side only
- Includes distributed test-runner
- Superceded by Jasmine, no longer maintained
Black Magic
- Standalone test spies, stubs and mocks
- Fake timers, XHR and servers
- No dependencies, works with any unit testing framework
- Sinon's creator authored Test-Driven JavaScript Development
"test should call mocked subscriber": function () {
var myAPI = { method: function () {} };
var mock = sinon.mock(myAPI);
mock.expects("method").once();
PubSub.subscribe("message", myAPI.method);
PubSub.publishSync("message", undefined);
mock.verify();
}
JS Test Runner
- Distributed parallel test runner
- Easy to add to CI configuration
- Provides code coverage
JS Test Runner
BDD for Node.js
- Async specs
- Ability to parallel-ise asynchronous specs
- Fast
vows.describe('Division by Zero').addBatch({
'when dividing a number by zero': {
topic: function () { return 42 / 0 },
'we get Infinity': function (topic) {
assert.equal(topic, Infinity);
}
},
'but when dividing zero by zero': {
topic: function () { return 0 / 0 },
'we get a value which': {
'is not a number': function (topic) {
assert.isNaN(topic);
},
'is not equal to itself': function (topic) {
assert.notEqual(topic, topic);
}
}
}
}).run();
BDD for Node.js
- Based on the Node.js assert module
- Async specs
- No parallel-isation support
- Runs client/server-side
exports.testSomething = function(test){
test.expect(1);
test.ok(true, "this assertion should pass");
test.done();
};
exports.testSomethingElse = function(test){
test.ok(false, "this assertion should fail");
test.done();
};
Backbone Model and View Testing
with Jasmine and Sinon.js
Backbone Testing Framework
Why Jasmine Dominates
- Easily extensible
- Async tests are easy
- Running client/server-side means Node.js require modules can be tested in both environments with
the same unit and test code
Backbone Testing Framework
Why Sinon.js Dominates
- Unbridled Power!
- Spies: functions that record the inner workings of new or existing functions (reflection on
meth)
- Stubs: spies with defined behaviour (i.e. defined return values, known exceptions)
- Mocks: unit-level stubs with pre-programmed expectations (i.e. number of calls, arguments)
- Timers: bending the four-dimensional continuum
- XHR/Server: XMLHttpRequest fakery
Backbone Models (and Collections)
Simple Declarations
window.MenuItem = Backbone.Model.extend({
defaults : {
"active": false
}
});
window.MenuItemCollection = Backbone.Collection.extend({
model: MenuItem,
url : "../api/project"
});
Backbone Models (and Collections)
Jasmine Model Unit Test Spec Setup
describe("MenuItem", function () {
var menuItem;
var menuItemCollection;
beforeEach(function () {
menuItem = new MenuItem();
menuItemCollection = new MenuItemCollection();
});
describe("MenuItem model", function () {
it("has default property 'active' set to false", function () {
expect(menuItem.get('active')).toEqual(false);
});
});
...
});
Backbone Models (and Collections)
Jasmine Collection Functional Test Spec
describe("MenuItem", function () {
...
describe("MenuItemCollection", function () {
it("has child models", function () {
menuItemCollection.fetch({async: false});
expect(menuItemCollection.models.length).toBeGreaterThan(0);
});
});
...
});
Backbone Models (and Collections)
Jasmine Collection Functional-to-Unit Test Spec with sinon-server.js
describe("MenuItem", function () {
...
describe("MenuItemCollection", function () {
it("has child models", function () {
menuItemCollection.fetch({async: false});
expect(menuItemCollection.models.length).toBeGreaterThan(0);
});
it("has child models from Mock", function () {
var server = sinon.fakeServer.create();
server.respondWith([200, {"Content-Type": "application/json"},
'[{"id":12345678, "title":"Sudo make me a sandwich"}]']);
menuItemCollection.fetch({async: false});
server.respond();
expect(menuItemCollection.models.length).toEqual(1);
server.restore();
});
});
...
});
jQuery matchers and fixture loader for Jasmine
-
toBe(jQuerySelector)
- e.g.
expect($('<div id="some-id"></div>')).toBe('div#some-id')
-
toBeChecked()
-
toBeEmpty()
toBeHidden()
-
toBeSelected()
- e.g.
expect($('<option selected="selected"></option>')).toBeSelected()
toBeVisible()
-
toContain(jQuerySelector)
- e.g.
expect($('<div><span class="some-class"></span></div>')).toContain('span.some-class')
toExist()
-
toHaveAttr(attributeName, attributeValue)
-
toHaveProp(propertyName, propertyValue)
-
toHaveBeenTriggeredOn(selector)
-
toHaveBeenPreventedOn(selector)
-
toHaveClass(className)
-
toHaveData(key, value)
-
toHaveHtml(string)
- e.g.
expect($('<div><span></span></div>')).toHaveHtml('<span></span>')
-
toHaveId(id)
- e.g.
expect($('<div id="some-id"></div>')).toHaveId("some-id")
-
toHaveText(string)
- accepts a String or regular expression
- e.g.
expect($('<div>some text</div>')).toHaveText('some text')
-
toHaveValue(value)
- only for tags that have value attribute
- e.g.
expect($('<input type="text" value="some text"/>')).toHaveValue('some text')
-
toBeDisabled()
- e.g.
expect('<input type='submit' disabled='disabled'/>').toBeDisabled()
-
toBeFocused()
- e.g.
expect($('<input type='text' />').focus()).toBeFocused()
-
toHandle(eventName)
- e.g.
expect($form).toHandle("submit")
-
And many more...
Backbone Views
Setup Views spec with Jasmine and jasmine-jquery.js
describe("MenuItem", function () {
...
beforeEach(function () {
menuItem = new MenuItem();
menuItemCollection = new MenuItemCollection();
});
...
describe("MenuItemCollection View", function () {
var view;
var mockMenuItemCollection;
beforeEach(function () {
mockMenuItemCollection = new MenuItemCollection(window.mock.menuItemCollection);
view = new MenuListView({model: mockMenuItemCollection});
});
...
});
Backbone Views
Testing Views with Jasmine and jasmine-jquery.js
describe("MenuItem", function () {
...
describe("MenuItemCollection View", function () {
...
it("has none selected by default", function () {
var find = $(view.render().el).find('.nav-list .active').length;
expect(find).toEqual(0);
});
it("sets menu item to active when clicked", function () {
mockMenuItemCollection.at(0).set({active: true});
var find = $(view.render().el).find('.active');
expect(find).toExist();
});
...
});
Go forth and Unit Test your Javascript!
Test-Driven JavaScript Development is definitive!