Testing Backbone + RequireJS Applications with Jasmine
In my previous post, I covered the structure of a Backbone.js application using RequireJS. The next thing (or if you are a TDD fan, the first thing) we do is to run some tests on it. According to Coda Hale:
Writing tests for your code may not reduce the number of bugs, but it will make fixing the bugs you inevitably find easier.
Obviously, we can take advantage of the AMD architecture, to help us write modular tests (or “specs” in the BDD language). To get a better sense of the challenges and different approaches in unit testing, I wrote the exact same tests three times using three different testing frameworks: Jasmine, Mocha and QUnit.
The Concept
The concept is fairly simple. We want to test each one of our modules (Models, Collections, Views) and validate they behave as expected. We will have one html file, on SpecRunner javascript file, which will initialize the test suite, and a spec file per AMD module.
Since the concept and the implementation are so similar for the 3 frameworks, I’ll focus on Jasmine in this post. the code for the 3 of them is available in the GitHub repo.
index.html
This is how our Jasmine html file looks like. there’s not a lot I can say about this.
I added a hidden (or almost hidden) element with the id “sandbox” where all the DOM elements will be appended to while testing, each view test will append itself into this element, and will remove itself when done testing. I kept this element visible with very little height so a selector such as jQuery.is(“:visible”) will be valid. There are many other ways to accomplish this.
Jasmine Spec Runner
<script src="/js/lib/require-2.0.5.js" data-main="SpecRunner"></script>
SpecRunner.js
Our spec runner is where the magic happens. It is responsible to
- Load the dependancies (core libraries, testing libraries)
- Configure the testing environment
- Load the test suites
- Run the test engine
Configure RequireJS
One important bit is we would like to keep our require.config with the same paths as our app, so we won’t need to change anything within our application modules. we will add an extra path to our spec (tests) folder, and keep the previous baseURL and paths identical to our app.js. Because our baseURL is /js/
, all the other paths will be relative to that. One more thing I added is a cache buster to all the module urls using the urlArgs
parameter. It’s a good idea to have this in the test environment, so we don’t need to worry about browser caching.
require.config({
baseUrl: '/js/',
urlArgs: 'cb=' + Math.random(),
paths: {
jquery: 'lib/jquery-1.8.0',
underscore: 'lib/underscore-1.3.3',
backbone: 'lib/backbone-0.9.2',
'backbone.localStorage': 'lib/backbone.localStorage',
jasmine: '../test/lib/jasmine',
'jasmine-html': '../test/lib/jasmine-html',
spec: '../test/jasmine/spec/'
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
},
'backbone.localStorage': {
deps: ['backbone'],
exports: 'Backbone'
},
jasmine: {
exports: 'jasmine'
},
'jasmine-html': {
deps: ['jasmine'],
exports: 'jasmine'
}
}
})
Configure and execute the test engine
Next, we will load our main dependencies using require() and setup Jasmine, require() our specs, and run Jasmine’s test engine.
require(['underscore', 'jquery', 'jasmine-html'], function (_, $, jasmine) {
var jasmineEnv = jasmine.getEnv()
jasmineEnv.updateInterval = 1000
var htmlReporter = new jasmine.HtmlReporter()
jasmineEnv.addReporter(htmlReporter)
jasmineEnv.specFilter = function (spec) {
return htmlReporter.specFilter(spec)
}
var specs = []
specs.push('spec/models/TodoSpec')
specs.push('spec/views/ClearCompletedSpec')
specs.push('spec/views/CountViewSpec')
specs.push('spec/views/FooterViewSpec')
specs.push('spec/views/MarkAllSpec')
specs.push('spec/views/NewTaskSpec')
specs.push('spec/views/TaskListSpec')
specs.push('spec/views/TaskViewSpec')
$(function () {
require(specs, function () {
jasmineEnv.execute()
})
})
})
The structure of a test suite
Jasmine, like most frameworks has a ‘beforeEach’ and ‘afterEach’ methods, which will run before and after each test. In this spec, we test our ‘remaining items counter’ view, so, in our beforeEach method, we want to initialize the view and the todos collection the view consumes. The afterEach will remove the view from the DOM.
Jasmine also integrates spies that permit many spying, mocking, and faking behaviors.
Async
One significant difference between Jasmine and Mocha is the way Jasmine handles async specs. Instead of using a done() function in the test’s signature, Jasmine uses run(), wait(), and waitFor(). At first, this looked over complicated to me when compared to mocha’s simple approach, but once I got used to it, I’ve learned to appreciate the flexibility it gives you. For instance, in these cases your spec (or before/after) has more than one async call. I know what you’re thinking. You should never have more than one async call per spec. I agree, but life is not ideal, and implementations shouldn’t set the limits.
Since a test suite will never be a dependency, and the Jasmine’s describe()
method, that defines a test suite, already has a modular structure, there is not real need of wrapping it in a define statement.
describe('View :: Count Remaining Items', function () {
beforeEach(function () {
var flag = false,
that = this
require(['models/Todo', 'views/CountView'], function (Todo, View) {
that.todos = new Todo.Collection()
that.view = new View({ collection: that.todos })
that.mockData = {
title: 'Foo Bar',
timestamp: new Date().getTime()
}
$('#sandbox').html(that.view.render().el)
flag = true
})
waitsFor(function () {
return flag
})
})
afterEach(function () {
this.view.remove()
})
describe('Shows And Hides', function () {
it('should be hidden', function () {
expect(this.view.$el.is(':visible')).toEqual(false)
})
it('should toggle on add', function () {
this.todos.add(this.mockData)
expect(this.view.$el.is(':visible')).toEqual(true)
})
})
describe('Renders Text', function () {
it('should be empty', function () {
expect(this.view.$el.text()).toEqual('')
})
it('should re-render on add', function () {
this.todos.add(this.mockData)
expect(this.view.$el.text()).toEqual('1 item left')
this.todos.add([this.mockData, this.mockData])
expect(this.view.$el.text()).toEqual('3 items left')
})
})
})
You get the point. The full running Jasmine test suite of the Todos app can be found here. The source code for this test suite can be found here
Edit, Jan 2013: Pointed by Andreas Boehrnsen, a better way to write the specs would be to define the dependencies in a define block.
define(['models/Todo', 'views/CountView'], function (Todo, View) {
return describe('View :: Count Remaining Items', function () {
beforeEach(function () {
that.todos = new Todo.Collection()
that.view = new View({ collection: that.todos })
that.mockData = {
title: 'Foo Bar',
timestamp: new Date().getTime()
}
$('#sandbox').html(that.view.render().el)
})
// ...
})
})
The async part remains a good tool to have for other async cases.
Conclusions
RequireJS not only help handle dependencies and code organization in applications, it also makes testing easier and more fun. Modular code is clearly broken in to “units” which makes it very easy to unit test.