##Introduction
One definition of unit testing is the process of taking the smallest piece of testable code in an application, isolating it from the remainder of your codebase and determining if it behaves exactly as expected. In this section, we'll be taking a look at how to unit test Backbone applications using a popular JavaScript testing framework called Jasmine.
For an application to be considered 'well'-tested, distinct functionality should ideally have its own separate unit tests where it's tested against the different conditions you expect it to work under. All tests must pass before functionality is considered 'complete'. This allows developers to both modify a unit of code and it's dependencies with a level of confidence about whether these changes have caused any breakage.
As a basic example of unit testing is where a developer may wish to assert whether passing specific values through to a sum
function results in the correct output being returned. For an example more relevant to this book, we may wish to assert whether a user adding a new Todo item to a list correctly adds a Model of a specific type to a Todos Collection.
When building modern web-applications, it's typically considered best-practice to include automated unit testing as a part of your development process. Whilst we'll be focusing on Jasmine as a solution for this, there are a number of other alternatives worth considering, including QUnit.
##Jasmine
Jasmine describes itself as a behaviour-driven development (BDD) framework for testing JavaScript code. Before we jump into how the framework works, it's useful to understand exactly what BDD is.
BDD is a testing approach first described by Dan North which attempts to help both developers and non-technical stakeholders:
- Better understand and represent the models of the problems being solved
- Explain supported tests cases in a language that non-developers can read
- Focus on minimizing translation of the technical code being written and the domain language spoken by the business
What this means is that developers should be able to show Jasmine unit tests to a project stakeholder and (at a high level, thanks to a common vocabulary being used) they'll ideally be able to understand what the code supports.
That said, I often find developers using Jasmine for test-driven development (TDD) instead. The main idea behind TDD is:
- Write unit tests which describe the functionality you would like your code to support
- Watch these tests fail (as the code to support them hasn't yet been written)
- Write code to make the tests pass
- Rinse, repeat and refactor
I encourage you to try out both approaches to establish which works best for you, but for the purposes of this section we're going to opt for TDD with a little BDD thrown in for good measure.
Note: I've seen a lot of developers also opt for writing tests to validate behaviour of their code after having written it. While this is fine, note that it can come with pitfalls such as only testing for behaviour your code currently supports, rather than behaviour the problem needs to be supported.
##Suites, Specs & Spies
When using Jasmine, you'll be writing suites and specifications (specs). Suites basically describe scenarios whilst specs describe what can be done in these scenarios.
Each spec is a JavaScript function, described with a call to ```it()`` using a description string and a function. The description should describe the behaviour the particular unit of code should exhibit and keeping in mind BDD, it should ideally be meaningful. Here's an example of a basic spec:
it('should be incrementing in value', function(){
var counter = 0;
counter++;
});
On it's own, a spec isn't particularly useful until expectations are set about the behaviour of the code. Expectations in specs are defined using the expect()
function and an expectation matcher (e.g toEqual(), toBeTruthy(), toContain()). A revised example using an expectation matcher would look like:
it('should be incrementing in value', function(){
var counter = 0;
counter++;
expect(counter).toEqual(1);
});
The above code passes our behavioural expectation as ```counter`` equals 1. Notice how easy this was to read the expectation on the last line (you probably grokked it without any explanation).
Specs are grouped into suites which we describe using Jasmine's describe()
function, again passing a string as a description and a function. The name/description for your suite is typically that of the component or module you're testing.
Jasmine will use it as the group name when it reports the results of the specs you've asked it to run. A simple suite containing our sample spec could look like:
describe('Stats', function(){
it('can increment a number', function(){
...
});
it('can subtract a number', function(){
...
});
});
Suites also share a functional scope and so it's possible to declare variables and functions inside a describe block which are accessible within specs:
```javascript
describe('Stats', function(){
var counter = 1;
it('can increment a number', function(){
// the counter was = 1
counter = counter + 1;
expect(counter).toEqual(2);
});
it('can subtract a number', function(){
// the counter was = 2
counter = counter + 3;
expect(counter).toEqual(5);
});
});
Note: Suites are executed in the order in which their are described, which can be useful to know if you would prefer to see test results for specific parts of your application reported first.
Jasmine also supports 'spies' - a way to mock, spy and fake behaviour in our unit tests. Spies replace the function they're spying on, allowing us to simulate behaviour we would like to mock (i.e test free of the actual implementation).
In the below example, we're spying on the 'setComplete' method of a dummy Todo function to test that arguments can be passed to it as expected.
var Todo = function(){
};
Todo.prototype.setComplete = function (arg){
return arg;
}
describe('a simple spy', function(){
it('should spy on an instance method of a Todo', function(){
var myTodo = new Todo();
spyOn(myTodo, 'setComplete');
myTodo.setComplete('foo bar');
expect(myTodo.setComplete).toHaveBeenCalledWith('foo bar');
var myTodo2 = new Todo();
spyOn(myTodo2, 'setComplete');
expect(myTodo2.setComplete).not.toHaveBeenCalled();
});
});
What you're more likely to use spies for is testing asynchronous behaviour in your application such as AJAX requests. Jasmine supports:
- Writing tests which can mock AJAX requests using spies. This allows us to test code which runs before an AJAX request and right after. It's also possible to mock/fake responses the server can return and the benefit of this type of testing is that it's faster as no real calls are being made to a server
- Asynchronous tests which don't rely on spies
For the first kind of test, it's possible to both fake an AJAX request and verify that the request was both calling the correct URL and executed a callback where one was provided.
it("the callback should be executed on success", function () {
spyOn($, "ajax").andCallFake(function(options) {
options.success();
});
var callback = jasmine.createSpy();
getTodo(15, callback);
expect($.ajax.mostRecentCall.args[0]["url"]).toEqual("/todos/15");
expect(callback).toHaveBeenCalled();
});
function getTodo(id, callback) {
$.ajax({
type: "GET",
url: "/todos/" + id,
dataType: "json",
success: callback
});
}
If you feel lost having seen matchers like andCallFake()
and toHaveBeenCalled()
, don't worry. All of these are Spy-specific matchers and are documented on the Jasmine wiki.
For the second type of test (asynchronous tests), we can take the above further by taking advantage of three other methods Jasmine supports:
- runs(function) - a block which runs as if it was directly called
- waits(timeout) - a native timeout before the next block is run
- waitsFor(function, optional message, optional timeout) - a way to pause specs until some other work has completed. Jasmine waits until the supplied function returns true here before it moves on to the next block.
it("should make an actual AJAX request to a server", function () {
var callback = jasmine.createSpy();
getTodo(16, callback);
waitsFor(function() {
return callback.callCount > 0;
});
runs(function() {
expect(callback).toHaveBeenCalled();
});
});
function getTodo(id, callback) {
$.ajax({
type: "GET",
url: "todos.json",
dataType: "json",
success: callback
});
}
##beforeEach and afterEach()
Jasmine also supports specifying code that can be run before each (beforeEach()
) and after each (afterEach
) test. This is useful for enforcing consistent conditions (such as resetting variables that may be required by specs). In the following example, beforeEach()
is used to create a new sample Todo model specs can use for testing attributes.
beforeEach(function(){
this.todo = new Backbone.Model({
text: "Buy some more groceries",
done: false
});
});
it("should contain a text value if not the default value", function(){
expect(this.todo.get('text')).toEqual("Buy some more groceries");
});
Each nested describe()
in your tests can have their own beforeEach()
and afterEach()
methods which support including setup and teardown methods relevant to a particular suite. We'll be using beforeEach()
in practice a little later.
##Getting setup
Now that we've reviewed some fundamentals, let's go through downloading Jasmine and getting everything setup to write tests.
A standalone release of Jasmine can be downloaded from the official release page.
You'll need a file called SpecRunner.html in addition to the release. It can be downloaded from https://github.com/pivotal/jasmine/tree/master/lib/jasmine-core/example or as part of a download of the complete Jasmine repo.Alternatively, you can git clone
the main Jasmine repository from https://github.com/pivotal/jasmine.git.
Let's review SpecRunner.html:
It first includes both Jasmine and the necessary CSS required for reporting:
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.1.0.rc1/jasmine.css">
<script type="text/javascript" src="lib/jasmine-1.1.0.rc1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.1.0.rc1/jasmine-html.js"></script>
Next, some sample tests are included:
<script type="text/javascript" src="spec/SpecHelper.js"></script>
<script type="text/javascript" src="spec/PlayerSpec.js"></script>
And finally the sources being tested:
<script type="text/javascript" src="src/Player.js"></script>
<script type="text/javascript" src="src/Song.js"></script>
Note: Below this section of SpecRunner is code responsible for running the actual tests. Given that we won't be covering modifying this code, I'm going to skip reviewing it. I do however encourage you to take a look through PlayerSpec.js and SpecHelper.js. They're a useful basic example to go through how a minimal set of tests might work.
##TDD With Backbone
When developing applications with Backbone, it can be necessary to test both individual modules of code as well as modules, views, collections and routers. Taking a TDD approach to testing, let's review some specs for testing these Backbone components using the popular Backbone Todo application. I would like to extend my thanks to Larry Myers for his Koans project which greatly helped here. I recommend downloading Backbone-koans for usage with this section as you'll get the most learning value using it alongside.
##Models
The complexity of Backbone models can vary greatly depending on what your application is trying to achieve. In the following example, we're going to test default values, attributes, state changes and validation rules.
First, we begin our suite for model testing using describe()
:
describe('Tests for Todo', function() {
Models should ideally have default values for attributes. This helps ensure that when creating instances without a value set for any specific attribute, a default one (e.g "") is used instead. The idea here is to allow your application to interact with models without any unexpected behaviour.
In the following spec, we create a new Todo without any attributes passed then check to find out what the value of the text
attribute is. As no value has been set, we expect a default value of ```""`` to be returned.
it('Can be created with default values for its attributes.', function() {
var todo = new Todo();
expect(todo.get('text')).toBe("");
});
If testing this spec before your models have been written, you'll incur a failing test, as expected. What's required for the spec to pass is a default value for the attribute text
. We can implement this default value with some other useful defaults (which we'll be using shortly) in our Todo model as follows:
window.Todo = Backbone.Model.extend({
defaults: function() {
return {
text: "",
done: false,
order: 0
};
}
Next, we want to test that our model will pass attributes that are set such that retrieving the value of these attributes after initialization will be what we expect. Notice that here, in addition to testing for an expected value for text
, we're also testing the other default values are what we expect them to be.
it('Will set passed attributes on the model instance when created.', function() {
var todo = new Todo({ text: 'Get oil change for car.' });
// what are the values expected here for each of the
// attributes in our Todo?
expect(todo.get('text')).toBe("Get oil change for car.");
expect(todo.get('done')).toBe(false);
expect(todo.get('order')).toBe(0);
});
Backbone models support a model.change() event which is triggered when the state of a model changes. In the following example, by 'state' I'm referring to the value of a Todo model's attributes. The reason changes of state are important to test are that there may be state-dependant events in your application e.g you may wish to display a confirmation view once a Todo model has been updated.
it('Fires a custom event when the state changes.', function() {
var spy = jasmine.createSpy('-change event callback-');
var todo = new Todo();
// how do we monitor changes of state?
todo.bind('change', spy);
// what would you need to do to force a change of state?
todo.set({ text: 'Get oil change for car.' });
expect(spy).toHaveBeenCalled();
});
It's common to include validation logic in your models to ensure both the input passed from users (and other modules) in the application are 'valid'. A Todo app may wish to validate the text input supplied in case it contains rude words. Similarly if we're storing the done
state of a Todo item using booleans, we need to validate that truthy/falsy values are passed and not just any arbitrary string.
In the following spec, we take advantage of the fact that validations which fail model.validate() trigger an "error" event. This allows us to test if validations are correctly failing when invalid input is supplied.
We create an errorCallback spy using Jasmine's built in createSpy()
method which allows us to spy on the error event as follows:
it('Can contain custom validation rules, and will trigger an error event on failed validation.', function() {
var errorCallback = jasmine.createSpy('-error event callback-');
var todo = new Todo();
todo.bind('error', errorCallback);
// What would you need to set on the todo properties to
// cause validation to fail?
todo.set({done:'a non-integer value'});
var errorArgs = errorCallback.mostRecentCall.args;
expect(errorArgs).toBeDefined();
expect(errorArgs[0]).toBe(todo);
expect(errorArgs[1]).toBe('Todo.done must be a boolean value.');
});
The code to make the above failing test support validation is relatively simple. In our model, we override the validate() method (as recommended in the Backbone docs), checking to make sure a model both has a 'done' property and is a valid boolean before allowing it to pass.
validate: function(attrs) {
if (attrs.hasOwnProperty('done') && !_.isBoolean(attrs.done)) {
return 'Todo.done must be a boolean value.';
}
}
If you would like to review the final code for our Todo model, you can find it below:
var NAUGHTY_WORDS = /crap|poop|hell|frogs/gi;
function sanitize(str) {
return str.replace(NAUGHTY_WORDS, 'rainbows');
}
window.Todo = Backbone.Model.extend({
defaults: function() {
return {
text: '',
done: false,
order: 0
};
},
initialize: function() {
this.set({text: sanitize(this.get('text'))}, {silent: true});
},
validate: function(attrs) {
if (attrs.hasOwnProperty('done') && !_.isBoolean(attrs.done)) {
return 'Todo.done must be a boolean value.';
}
},
toggle: function() {
this.save({done: !this.get("done")});
}
});
##Collections
We now need to define specs to tests a Backbone collection of Todo models (a TodoList). Collections are responsible for a number of list tasks including managing order and filtering.
A few specific specs that come to mind when working with collections are:
- Making sure we can add new Todo models as both objects and arrays
- Attribute testing to make sure attributes such as the base URL of the collection are values we expect
- Purposefully adding items with a status of
done:true
and checking against how many the collection thinks have been completed vs. those that are remaining
In this section we're going to cover the first two of these with the third left as an extended exercise I recommend trying out.
Testing Todo models can be added to a collection as objects or arrays is relatively trivial. First, we initialize a new TodoList collection and check to make sure it's length (i.e the number of Todo models it contains) is 0. Next, we add new Todos, both as objects and arrays, checking the length property of the collection at each stage to ensure the overall count is what we expect:
describe('Tests for TodoList', function() {
it('Can add Model instances as objects and arrays.', function() {
var todos = new TodoList();
expect(todos.length).toBe(0);
todos.add({ text: 'Clean the kitchen' });
// how many todos have been added so far?
expect(todos.length).toBe(1);
todos.add([
{ text: 'Do the laundry', done: true },
{ text: 'Go to the gym'}
]);
// how many are there in total now?
expect(todos.length).toBe(3);
});
...
Similar to model attributes, it's also quite straight-forward to test attributes in collections. Here we have a spec that ensures the collection.url (i.e the url reference to the collection's location on the server) is what we expect it to be:
it('Can have a url property to define the basic url structure for all contained models.', function() {
var todos = new TodoList();
// what has been specified as the url base in our model?
expect(todos.url).toBe('/todos/');
});
For the third spec, it's useful to remember that the implementation for our collection will have methods for filtering how many Todo items are done and how many are remaining - we can call these done()
and remaining()
. Consider writing a spec which creates a new collection and adds one new model that has a preset done
state of true
and two others that have the default done
state of false
. Testing the length of what's returned using done()
and remaining()
should allow us to know whether the state management in our application is working or needs a little tweaking.
The final implementation for our TodoList collection can be found below:
window.TodoList = Backbone.Collection.extend({
model: Todo,
url: '/todos/',
done: function() {
return this.filter(function(todo) { return todo.get('done'); });
},
remaining: function() {
return this.without.apply(this, this.done());
},
nextOrder: function() {
if (!this.length) {
return 1;
}
return this.last().get('order') + 1;
},
comparator: function(todo) {
return todo.get('order');
}
});
##Views
The setup for our view specs uses beforeEach()
to append both an empty UL
(#todoList) to the DOM and initialize a new instance of a TodoView using an empty Todo model. afterEach()
is used to remove the previous #todoList UL
as well as the previous instance of the view. This helps ensure that a consistant testing setup is available for each spec.
describe('Tests for TodoView', function() {
var todoView;
beforeEach(function() {
$('body').append('<ul id="todoList"></ul>');
todoView = new TodoView({ model: new Todo() });
});
afterEach(function() {
todoView.remove();
$('#todoList').remove();
});
...
The first spec that's useful to write is a check that the TodoView we've created is using the correct tagName
(element or className). The purpose of this test is to make sure it's been correctly tied to a DOM element when it was created.
Backbone views typically create empty DOM elements once initialized, however these elements aren't attached to the visible DOM to allow them to be constructed without an impact on the performance of rendering.
it('Should be tied to a DOM element when created, based off the property provided.', function() {
//what html element tag name represents this view?
expect(todoView.el.tagName.toLowerCase()).toBe('li');
});
Once again, if the TodoView hasn't already been written, we'll experience failing specs which we'll need to write a solution for. In this (simple) case, it's as simple as creating a new Backbone.View with a specific tagName
.
var todoView = Backbone.View.extend({
tagName: "li"
});
You may have noticed that in beforeEach()
, we passed our view an initial (albeit unfilled) Todo model. Views should be backed by a model instance which provides data. As this is quite important to our view's ability to function, we can write a spec to ensure a model is both defined (using the toBeDefined()
matcher) and then test attributes of the model to ensure defaults both exist and are the value we expect them to be.
it('Is backed by a model instance, which provides the data.', function() {
expect(todoView.model).toBeDefined();
// what's the value for Todo.get('done') here?
expect(todoView.model.get('done')).toBe(false); //or toBeFalsy()
});
Finally, we want to test that elements being dynamically rendered and appended to the DOM contain all of the child elements we're expecting (e.g with a Todo item, we expect a checkbox to render as this is how users are able to mark whether an item is now 'done'). We also want to make sure our model's state remains in sync with the user's changes, so we also want to test the effects of clicking a Todo item's checkbox. To write this spec, we:
- Directly append the rendered output of our view to the #todoList (using
run()
, which as we've covered are blocks which run themselves as if they were directly called) - Filter the #todoList for the first input checkbox. If correctly appended, we should expect the view to contain DOM elements such as this so users can interact with them (if visible)
- Simulate clicking the Todo's checkbox to mark it as done/complete
- Check that the
done
state of the model is now true (or truthy)
it('Can use an events hash to wire up view methods to DOM elements.', function() {
var viewElt;
runs(function() {
$('#todoList').append(todoView.render().el);
});
waitsFor(function() {
viewElt = $('#todoList li input.check').filter(':first');
return viewElt.length > 0;
}, 1000, 'Expected DOM Elt to exist');
runs(function() {
// Hint: How would you trigger the view, via a DOM Event, to toggle the 'done' status?
// (See todos.js line 70, where the events hash is defined.)
// The answer is using: http://api.jquery.com/click
viewElt.click();
expect(todoView.model.get('done')).toBe(true);
});
});
##Events
describe('About Backbone.Events', function() {
var obj = {};
_.extend(obj, Backbone.Events);
beforeEach(function() {
obj.unbind(); // remove all custom events before each spec is run.
});
it('Can extend javascript objects to support custom events.', function() {
var basicObject = {};
_.extend(basicObject, Backbone.Events);
expect(basicObject.bind).not.toBeDefined();
expect(basicObject.unbind).not.toBeDefined();
expect(basicObject.trigger).not.toBeDefined();
});
it('Allows us to bind and trigger custom named events on an object.', function() {
var callback = jasmine.createSpy();
obj.bind('basic event', callback);
// how would you cause the callback for this custom event to be called?
obj.trigger('basic event');
expect(callback).toHaveBeenCalled();
});
it('Also passes along any arguments to the callback when an event is triggered.', function() {
var passedArgs = [];
obj.bind('some event', function() {
for (var i = 0; i < arguments.length; i++) {
passedArgs.push(arguments[i]);
}
});
obj.trigger('some event', 1, 2);
// how many arguments were passed along to some event?
expect(passedArgs.length).toBe(2);
});
it('Can also bind the passed context to the event callback.', function() {
var foo = { color: 'blue' };
var changeColor = function() {
this.color = 'red';
}
obj.bind('an event', changeColor, foo);
obj.trigger('an event');
// what do we expect the color to be now?
expect(foo.color).toBe('red');
});
});
For more on writing specs for routing, see this excellent article at TinnedFruit: http://tinnedfruit.com/2011/04/26/testing-backbone-apps-with-jasmine-sinon-3.html