Testing Backbone applications with Jasmine and Sinon Part 3: Routers and Views
Overview
This is the third and final part in a series of articles demonstrating how to test a Backbone.js application, employing the Jasmine BDD test framework and the Sinon.JS spying, stubbing and mocking library If you haven’t yet read the first or second parts, take a look now.
In this final part, we’ll be looking at some methods for unit testing Backbone routers and views. These object types both present their own unique challenges for testing, but Jasmine BDD and Sinon.JS provide the tools we need to isolate them and fake external code and system dependencies. We will be examining the following:
- different approaches to testing Backbone routes
- methods for testing view rendering
- using DOM fixtures in your specs
- using the jasmine-jquery plugin
- testing view event handlers
- using fake timers to manipulate timed events
Routers
Backbone.js router objects are responsible for URL hash routing within your application, and can also be used for initialisation tasks if that’s how you choose to structure your code.
When a URL route is matched in your application, Backbone calls the router method associated with the route. It also triggers a route event in the form route:[action]
where action
is the name of your method.
Whether you use a router method or set up event handlers to bind to the route event is up to you. I have had some success using event handlers for routes, as you can then delegate behaviour to the specific objects in the application that need to respond. Single route methods can become monolithic and difficult to test in large applications.
For this example, however, we’ll use simple route methods. Our approach will be to test two aspects of the router: firstly we’ll test the route URLs themselves to make sure a particular URL will fire a particular route method; and secondly we’ll look at directly testing router methods.
Example 1: Testing routes
Our todo application will be driven by routes. When a user navigates to the home page for the first time, we want to display their to do list. In our code, the steps required are as follows:
- The AppRouter responds to the home page route (represented by an empty hash)
- The
home
route method instantiates aTodoListView
and aTodos
collection (created in part 2 of this article). - The
Todos
collection is asked to fetch its contents from the server. - When this response is received, the
TodoListView
renders the list. - The rendering of each individual
Todo
item is delegated to new instances of aTodoView
object.
That’s quite a lot of code to test. The router is responsible for the first three of these steps. Firstly we’ll look at how to test whether a router responds correctly to a particular URL. This could potentially be tricky, as the Backbone.js routing system responds to changes in the browser address field. It might be possible to directly manipulate the browser address, but Backbone 0.5 and above provides a navigate
method on router objects that can be used to simulate a URL change.
Normally in an application you would instantiate a router once per page load, and run Backbone.history.start()
to start Backbone’s route listening. However, Backbone will only allow you to run Backbone.history.start()
once for each page load. Running it a second time will result in an error being thrown.
The simplest way around this is to wrap the call to Backbone.history.start()
in a try/catch block.
Here’s a spec:
AppRoutes.spec.js
:
The spec binds the route:index
event to an anonymous Sinon.JS spy function, allowing us to track whether and how it was called. We then ensure that the URL fragment has the value we want to test, in this case, an empty value. Calling Backbone.history.start()
would normally trigger an initial Backbone.js routing check. However, by passing an option hash that includes silent: true
we avoid the immediate route match. Note that we are also optionally using HTML5 pushState for browsers that support it.
The example itself triggers the route matching by calling the navigate
method on the router with the URL fragment as the first argument. If a truthy second argument is passed, Backbone will also call any matching route methods and trigger route events.
To ensure that the route method and event is always fired, we navigate away somewhere else silently during the setup phase, just to ensure that the URL fragments are different.
Once the routing check has been performed, we expect that our route spy has been called once, and that it has been called with no arguments, as there will be no parameters associated with the home route.
When the example is run, we get an expected error:
ReferenceError: AppRouter is not defined
Let’s fix this by creating our AppRouter
. Don’t forget to include it in jasmine.yml
if necessary:
AppRouter.js
:
Running the specs again produces the following error:
TypeError: Cannot call method 'navigate' of undefined
Hmm, for some reason Backbone.history
is undefined, and so there is no navigate
method on it. It turns out that Backbone.js creates an instance of Backbone.History
(upper case ‘H’) called Backbone.history
(lower case ‘h’) once a router has been created that has at least one route specified on it. This makes sense, as history management is only required if there are routes to respond to.
We can now create our route:
AppRouter.js
:
and our spec passes.
Now that our index route is being tested successfully, lets try the todo detail route. At some point, we’ll want to show the user details of a particular to do item. For example, some notes, tags and scheduling information might be displayed. The URL fragment for showing this detailed view would be todo/1
for a Todo
with an id
of 1. Let’s write a spec to test that our router handles this successfully.
AppRoutes.spec.js
:
This spec is very similar to the one for the home route, but we are now binding a spy to the route:todo
event and testing that the routeSpy
is called with the id
parameter from the URL.
This fails with the following messages:
Expected Function to have been called once.
Expected Function to have been called with '1'.
This is exactly what we were expecting. Now let’s create the route:
AppRouter.js
:
And again, we’re green. Simply by adding the route to the hash and creating an empty callback ensures that the route:todo
event is fired when the URL hash matches.
We could enhance these specs by ensuring that only numerical values are valid for the id
, and we could also check that our route methods are actually called by wrapping them with a Sinon.JS spy.
Now that we have some routes, we need to test that our route methods are behaving as they should be.
Example 2: Testing router methods
Once we have tested that the correct routes are actually being fired, we can test route methods simply by calling them. To test our index
method, we need to ensure that it instantiates a TodoListView
and a Todos
collection in the correct way. We’ll need to create fake objects for both.
AppRouter.spec.js
:
First we create our router instance for testing. We then create a bare Backbone.js Collection object to act as the Todos
collection that will be returned when we stub out its constructor function. Finally, we create Sinon.JS stubs for both the TodoListView
constructor and the Todos
collection constructor, returning a new Backbone.js View and our bare collection respectively.
Now to write the specs:
AppRouter.spec.js
:
Before each spec, we call our index
method for testing.
In the first spec we check that the Todos
collection constructor has been called exactly once, and that it was called with no arguments.
In the second spec, we check that the TodoListView
constructor was also called once, and that it was called with a hash object containing our stubbed collection instance. In this way we are testing that the application is linking the TodoListView
with its data source, the Todos
collection.
When these specs run, we get four failures:
creates a Todo list collection
Expected Function to have been called once.
Expected Function to have been called with exactly.
creates a Todo list view
Expected Function to have been called once
Expected Function to have been called with ...
So, let’s write the code to make these pass:
AppRouter.js
:
Simple. We now need to test that collection’s data is fetched when the index
route is run. This is done by simply calling the Todos
collection’s fetch
method. Let’s write another spec.
First, we need to stub the collection’s fetch
method so that it performs no action, but allows us to spy on it. We add the following line to our beforeEach
method just after creating this.collection
:
AppRouter.spec.js
:
We can then add our new spec after the previous two:
This fails as expected:
fetches the Todo list from the server
Expected Function to have been called once.
Expected Function to have been called with.
And making the spec pass is simple:
AppRouter.js
:
Our examples so far have been simple. You can see that routers can easily create a lot of other objects, and then call methods on those objects in order to get things rolling in your application.
If you are instantiating your initial application objects like this in your routers, then you’ll be creating a lot of stubs and mocks in your router specs. This is a matter of application design. For simple applications it is probably not a big issue, but this approach soon gets unwieldy.
An alternative approach is to instantiate any initial Backbone.js objects in an overall application initialisation method that is run when the page is first loaded, for example in a DOM ready handler. The router would also be instantiated and Backbone.js’s history object initialised at this point. The primary application objects that you have created (usually the views) can then bind and unbind to the built-in Backbone.js route events as required within their own code. In this way you are effectively delegating responsibility to the individual application objects so they are in charge of their own destiny. The outcome of this is code that is easier to test, and easier to maintain. If your specs become unwieldy, long and difficult to set up, then this is often a code smell suggesting that you should probably simplify or refactor your code.
Looking back to the top of example 1, we can see that we have now tested the first three steps required to render our to do list. The last two steps are the responsibility of two views: the TodoListView
and the TodoView
. Let’s take a look at testing views, then.
Views
Because our app uses jQuery for DOM manipulation, it makes some sense to use jQuery to test the rendered elements that our views will produce. Fortunately there is a Jasmine BDD jQuery plugin specifically for this purpose. The plugin provides two key features: firstly there are a number of Jasmine matchers to test jQuery wrapped sets and elements and secondly, it provides the ability to create temporary HTML fixtures for your specs to use.
To use the plugin, just include the jasmine-jquery.js
file in your jasmine.yml
or SpecRunner.html
.
Example 1: Creating the root element
In our first view example, we’ll create a simple spec to check that our TodoListView
has created the expected element when it is initialised. Backbone.js views will create an empty DOM element as soon as they are initialised, but this element will not be attached to the visible DOM. This allows a view to be constructed without unduly affecting rendering performance.
Our spec is pretty simple:
TodoListView.spec.js
:
Running this spec produces the following failure:
Expected 'DIV' to equal 'UL'.
We can fix this easily in our TodoListView.js
by specifying the built-in Backbone.js tagName
property for the view:
TodoListView.js
:
Let’s also check that the element has the right class:
TodoListView.spec.js
:
This uses the toHaveClass
matcher created by the jasmine-jquery plugin which operates on jQuery objects. If we had not used the plugin, the expectation would have looked something like this:
which would produce a failure output like this:
Expected false to be truthy.
This is not very helpful for debugging purposes. Using the jasmine-jquery matcher produces this failure:
Expected '<ul></ul>' to have class 'todos'.
Again, we can easily fix this with a simple className
property on the view object.
TodoListView.js
:
Let’s move on to testing the actual rendering of our to do list content.
Example 2: Rendering
When we ask our to do list to render, it will create a task entry for each instance of a Todo
model in the Todos
collection. Each one of these tasks is a view instance with a reference to the model that will be rendered.
So, when the TodoListView
’s render()
method is called, we want to test that a TodoView
is instantiated for each model in the associated collection.
Once again, because we are not currently testing the TodoView
object, we will stub it with a basic Backbone.js view. As discussed in part 2 of this series, I find that this is by far the easiest way to isolate a Backbone.js object from other Backbone.js objects in your specs without resorting to mocking and stubbing the whole Backbone.js interface.
We create a basic Backbone.js view to stand in for the TodoView
, and then stub the TodoView
constructor function, returning our basic Backbone.js view instead of a real TodoView
instance.
We then create a simple Backbone.js collection with three models, and associate the TodoList
view instance with this collection. When the view’s render()
method is called, the expected behaviour is then to call the TodoView
constructor once for each model in the collection.
TodoListView.spec.js
:
Running this spec results in 3 errors:
TypeError: Attempted to wrap undefined property TodoView as function
TypeError: Cannot read property 'calledThrice' of undefined
TypeError: Cannot call method 'restore' of undefined
This is telling us that we need to create a TodoView
object.
TodoView.js
:
Now, when we re-run the specs, we get this failure:
Expected Function to have been called thrice.
and three of these:
Expected Function to have been called with {..}
Those are the proper failures. Let’s fix it by writing the code we need.
Great. This passes, and we are now creating three TodoViews
. However, nothing will be rendered in the page. We need to make sure each TodoView
’s render()
method is called.
Firstly, we need to spy on the fake TodoView
’s render()
method. We set this up in our beforeEach
function:
TodoListView.spec.js
:
and then the spec itself:
The failure that results from running this spec can be fixed with the following one line change added to the render()
method in TodosView.js
:
TodoView.js
:
However, we still need to append the rendered todo to our list. This is done with jQuery. We can either stub the jQuery append
method, or we can physically check that an element has been appended. To write a spec for this we first need to create a simple stubbed render
method on the TodoView
stub object that creates a DOM element and returns itself, like so:
TodoView.spec.js
:
and we can now write a spec to check that one of these elements has been appended for each model:
This produces the following failure as expected:
Expected 0 to equal 3.
Lets’ fix that in TodosView.js
:
TodosView.js
:
Running the specs produces the same failure as before. What happened? This is a common gotcha when first starting out with Backbone.js. Because the addTodo()
method is called as a callback from within an underscore.js each()
iterator, the scope for addTodo
is not the TodoListView
instance, but the todo
model instance that is the target of the iteration cycle. Because of this, there is no el
property on this
, and the append fails.
Fortunately underscore.js provides a convenience function to fix the scope for a method named bindAll()
. In a Backbone.js application it is best called within the initialize()
method. It takes the intended scope as the first argument, and one or more methods on the current scope that are to have their scope set:
This sets the scope for the addTodo()
method to be the TodosView
instance rather than the scope it was actually called with.
Now the jQuery append is being called on the correct object, and the spec passes.
Example 3: Rendering HTML
So far our views have not actually rendered anything. Our TodoListView
simply delegates the actual rendering of markup to the individual TodoView
objects below it. Let’s test that these TodoView
elements are rendered as expected.
We’ll start by just using some string manipulation to create HTML markup to be rendered using jQuery’s html()
method.
We will create two specs initially. The first will check that the view’s render()
method returns the view instance. This is necessary for chaining, and something that we have already expected in the specs for TodoListView
. The second spec will check that the produced HTML is exactly as expected based on the properties of the model instance that is associated with our TodoView
.
Our beforeEach
function for these specs simply creates a sample model, and then instantiates a TodoView
and associates it with the model.
TodoView.spec.js
:
When these specs are run, only the second one fails. The first spec that tests that the TodoView
instance is returned from render()
passes because Backbone.js does this by default, and we haven’t overwritten the render
method with our own version yet.
The second spec fails with the following message:
Expected '' to equal '<a href="#todo/1"><h2>My Todo</h2></a>'.
By default, the render()
method creates no markup. Let’s write a simple replacement for render()
:
TodoView.js
:
This simply specifies a string template and replaces some fields marked with double curly braces with their respective values from the associated model. Because we are returning the TodoView
instance from the method, the first spec also passes.
It hardly needs saying that using an HTML string to test against like this is fraught with problems. It is extremely brittle. If you were to change one tiny thing about your template, including white space, your spec would fail, even thought the rendered output would be the same. It will also become time consuming to maintain as your template becomes more complex.
It is far better to test your rendered output using jQuery to select and inspect attribute and text values, element counts and so on.
Let’s write specs that check some key aspects of the expected output. Again, we are using the custom matchers added by the jasmine-jquery plugin:
TodoView.spec.js
:
Now is a good time to take a look at fixture elements. So far, we have been setting jQuery expectations against the view’s el
property. This is absolutely fine in many circumstances, and may actually be preferable a lot of the time. However, at times you will need to actually render some markup into the document. The best way to handle this within your specs is to use fixtures, a feature provided by the jasmine-jquery plugin. Let’s re-write that last spec to use fixtures:
TodoView.spec.js
:
We are now appending the rendered todo item into the fixture, and setting expectations against the fixture rather than the view’s el
property. One reason you might need to do this is when a Backbone.js view is set up against a pre-existing DOM element. You would need to provide the fixture and test that the el
property is picking up the correct element when the view is instantiated.
Example 4: Rendering with a template library
We can now start to make the template a little more complex by including some conditional logic. When a todo item is marked as done, we want to provide some visual feedback to the user in the form of a different background colour, or perhaps by striking through the title. We’ll do this by attaching a class to the anchor.
Let’s write a spec to test that this happens.
TodoView.spec.js
:
This fails, as expected, with the following message:
Expected '<a href="#todo/1"><h2>My Todo</h2></a>'
to have class 'done'.
We could fix this in our existing render method like so:
TodoView.js
:
However, you can see that this will get cumbersome quickly. The more logic we have here, the more complexity we introduce. This is where a template library can come in handy. There are many available, and exploring the options is beyond the scope of this article. For this example we’ll use Handlebars.js.
We’ll need to add handlebars.js
to jasmine.yml
or SpecRunner.html
. We should be able to rewrite our render code and get the existing specs passing without changing very much.
Here’s our new TodoView
object, modified to use handlebars.js
:
TodoView.js
:
The initialize
method compiles a Handlebars template provided as a string in the instantiation. Another way to reference a template would be by placing it in the page HTML and obtaining it via its id
attribute, which is a common approach with Handlebars. In a real application, it would be preferable to use the latter approach, and have your specs load the real template in for testing. One way to do this if your project uses Ruby is to use the Evergreen gem to handle template loading for you.
For our purposes, we’ll continue to use the string injection approach. We add a new directory named templates to the spec
directory, and add a new file named todo-template.js
which looks like this:
todo-template.js
:
This simply creates or extends a templates object in the Jasmine scope for each test and adds a todo
property containing the Handlebars template we want to use.
We’ll need to add the templates folder reference to jasmine.yml
or SpecRunner.html
, and also update our existing specs slighly to provide the template when instantiating the TodoView
object:
TodoView.spec.js
:
All of the existing specs continue to pass with our new templating system in place, so we can now enhance the template with some logic for the done status:
todo-template.js
:
And that spec now passes as well.
Example 4: Events
Backbone.js views also allow the declaration of DOM events to be listened for and executed upon. The API to do this is simple: a hash of key/value pairs where the key is a string containing the event to be bound to and the selector to be used, and the value is the name of the method to use as the callback when the event is triggered.
For our Todo app, we will provide a small edit icon for each to do item, which when clicked will replace the title text with an editable input field. Let’s write a spec that checks for this behaviour:
TodoView.spec.js
:
This spec runs and fails with the following messages:
Expected '' to be visible.
Expected '<h2>My Todo</h2>' not to be visible.
To fix this, we first need to create the edit link and input field in our template:
todo-template.js
:
Then we add the events hash with our click event linked to an event handler:
TodoView.js
:
Don’t forget to add a _.bindAll
call to set the scope of the edit callback. Our specs are all green again, and we can move on.
Example 5: Animations and timing
Suppose that one of your esteemed designer colleagues deems that when the user clicks the edit icon, the title text should fade out and the input field should fade in over the course of half a second. Of course, you would think that this is unnecessary fluff in the user interface, but you are overruled and you must carry out the instruction to the full or be dismissed immediately.
When it comes to actually carrying out the designer’s instructions, the code could not be easier. We simply amend the edit method to use jQuery’s fadeIn
and fadeOut
methods:
Great! All is well until you run the specs and are greeted with the following failure message:
Expected '<h2 style="opacity: 1; ">My Todo</h2>'
not to be visible.
The spec is checking the visible state of the title heading immediately after the render()
method is called. We need to wait for half a second before we check the state to give the animation time to complete.
One way around this is to use Jasmine’s built-in support for asynchronous specs. The existing spec could be re-written like this:
TodoView.spec.js
:
With this approach, we’re waiting for 510 milliseconds between the click event and the expectations, which are wrapped in a runs()
call to make them run after the wait has completed.
That’s not so bad, and the spec passes now. However, write more than a few of these asynchronous timed specs and you’ll end up with a very slow-running spec suite. Our spec suite has gone from 0.15 seconds to 0.65 seconds just because of one spec.
To eliminate this delay we can use the fake timing abilities of Sinon.JS. Instead of actually waiting for half a second to pass, Sinon.JS allows us to fake the passing of time itself. Unfortunately it doesn’t actually manipulate the space/time continuum, which would have been very neat programming indeed, but simply mucks about with JavaScript’s native time-keeping methods such as setTimeout
and setInterval
.
We can re-write our spec to use Sinon.JS’s fake timers as follows:
TodoView.spec.js
:
Fake timers are initialised by calling sinon.useFakeTimers()
in the beforeEach
method. We must restore the native timer functions to their original state after our specs, so we create an afterEach
function which does this. Finally, the spec itself is responsible for moving the clock forward by a specified number of milliseconds before we run our expectations.
Now when our specs pass, they pass in around 0.15 seconds again. Even though our application is now using animations, it has had minimal effect on our specs. This is most definitely a Good Thing, as it gives designers and developers the flexibility to tweak interface characteristics such as animations without being unduly held back by the test suite.
The use of fake timers is not limited to animations, of course. They can be used wherever timing is important in your application. For example, you may have a regular polling request that updates some information every minute. Instead of letting a spec run for a full minute, or artificially changing the polling interval in your spec, you can forward the timer by a minute and test that another request has been made to the server.
Summary
Testing user interface behaviours and interactions can sometimes be daunting, and test suites quite often end up being slow-running or incomplete because of the unique challenges the UI of web applications present. Although some of the techniques here are specific to Backbone.js, many apply to jQuery and other rich web application interfaces in general.
Throughout this series of articles, we have concentrated on writing unit tests where individual JavaScript objects are tested in isolation. Your test suite should also include some integration tests where objects are tested in combination with each other, and functional tests where an actual running application is tested using an automated browser driver such as Selenium or Web Driver. There are a pretty large number of frameworks, libraries and drivers that satisfy this need, but they can be difficult to set up, bug-prone and a challenge to debug. For this reason, the unit test suite is essential to catch as many problems as possible as early as possible, and to write test cases when bugs are discovered.
I hope that this series of articles has given you some useful techniques to start testing your Backbone.js applications, and not to be daunted by the apparent complexity that that may present at first. Like any seemingly complex task, testing is simply a matter of breaking the task down into smaller, more manageable units, and using a toolset to make this process faster and more efficient. Happy testing!