27 Jun 2013 Testing Backbone Views with QUnit and Sinon
Unit-testing Backbone Views is hard. You need to cover enough for the test to be meaningful (for example DOM updates and server calls), without getting too tangled up in gory details. In this post I’ll talk about how we use QUnit and Sinon.JS to unit-test our Backbone views.
There’s already material out there on using these frameworks together, so I’m not going to get into setting things up. However, having done it for a year now I feel I’ve learnt enough additional tips, tricks and traps to warrant sharing them with the world.
This post will cover the three things you have to think about when testing Backbone Views, then talk about three strategies for simplifying your tests. I’ll also share some more general thoughts on Backbone’s suitableness for building large apps.
Before We Begin
If you haven’t started your project yet, consider not using Backbone.
It’s not that there’s anything inherently wrong with Backbone – in fact, Backbone.js has done a lot to advance the state of client-side Javascript development. It’s just that for anything non-trivial, you rapidly have to build stuff with Backbone that you shouldn’t really have to worry about. Things like memory management, view management, efficient DOM updates, bindings, managing data relationships, and…testing.
Sure, there’s a bunch of Backbone plugins for doing some of these things, but there’s little consensus on what’s best-practice, and getting them all to behave together can be arduous. Whilst I can imagine Backbone still being used for specialised cases, for standard meat-and-potatoes apps I am ready to look at alternatives like Ember or Angular. Angular’s built-in testing support is particularly of interest to me.
On another note, you may be wondering why we haven’t used Jasmine. There’s no particular reason, other than that we have a slight preference for QUnit’s more traditional unit testing syntax, whereas Jasmine has a BDD syntax. However, as far as I can tell, this is very much just a question of taste – to generalize a point made by Prag Dave, I think BDD vs xUnit is mostly a cat/dog thing.
That said, in the year since we made the decision, I think Jasmine has developed an edge in terms of growing popularity, so feel free to use it instead. Either way, you’ll probably have to use Sinon for at least some of your mocking.
Three Things You Have To Consider When Testing Backbone Views
OK, let’s get down to it. When it comes to testing Backbone Views, there’s three main factors that you have to take into consideration:
- The DOM
- How to mock server calls
- How to mock the clock
I’ll cover each of these in detail.
The DOM
For view tests to be meaningful, they need a DOM to interact with. Fortunately, QUnit tests run in a browser, be it a browser with a user interface (like Chrome or Firefox) or a headless browser (like PhantomJS). This is good because it means the tests have access to a DOM that they can manipulate. Put differently, when your Backbone views render, they’ll have a place they can render to.
Furthermore, triggering events on the DOM and then querying it isn’t too painful. In fact, a lot of the time you don’t have to worry about it at all – it just works if you’re using jQuery under the hood (Zepto.js will require a little more setup that I won’t detail here).
Consider the following example, where we’re testing a view that renders a field and a button. Specifically, we’re testing that the button only gets enabled if the field is set:
... test("next button is disabled unless date is set", function() { var view = new SomeViewBeingTested({ model: new ModelForTheView() }); // Note that we don't have to specify an element for the view // when testing it in isolation view.render(); // 'Next' button should be disabled by default var $nextButton = this.view.$("button.submit"); ok($nextButton.hasClass("disabled")); // Now set the date var $date = this.view.$("#date"); $date.val("July 28 2012"); $date.trigger(jQuery.Event("keyup")); // And check that the next button has been enabled ok(!$nextButton.hasClass("disabled")); }); ...
Note that we use the Backbone.View.$
method to search for elements within the view’s DOM fragment. This is more portable, and means that we don’t have to search the whole DOM.
Note also that in this example we reuse references to the button and fields. However, if we were re-rendering the view as part of the test, we couldn’t reuse the references, because new buttons and fields would be rendered. Say that we were testing whether a stateful Backbone View was able to remember that it is supposed to be displaying content or not:
... test("should remember whether it has been opened", function() { var view = new ViewBeingTested({ model: new ModelForView() }); view.render(); var $content = view.$(".content"); ok($content.hasClass("hide")); $toggle.trigger(jQuery.Event("click")); ok(!$content.hasClass("hide")); // re-rendering should remember whether the content is visible view.render(); ok(!$content.hasClass("hide")); // IS TESTING THE WRONG THING }); ...
If we reuse the old element reference, this test will always pass – even if it’s not re-rendering correctly the second time around. Instead, it’s important that we find the element again after we re-render:
... // re-rendering should remember whether the content is visible view.render(); $content = view.$(".content"); // Look the element up again ok(!view.$(".content").hasClass("hide")); ...
How to mock server calls
It’s important that your tests are able to mock up responses to the calls that your app makes to the backend. The good news is that Sinon.js makes this relatively easy.
That said, I find the Sinon API runs counter to my intuitions at times, especially when it comes to mocking up server calls. It allows you to define responses in advance, but won’t actually return them until you tell it to. For example, say that we’re testing that when a view is rendered, it loads the details for a user and displays them. For could test it as follows:
... test("UserView should show user", function() { this.server.respondWith("GET", "/users/1", [ 200, { "Content-Type": "application/json" }, JSON.stringify({id: 1, name: "John Tester"}) ]); var view = new UserView(new User({ id: 1 })); view.render(); this.server.respond(); equal(view.$(".name"), "John Tester"); });
However, often you can actually define a response when it’s needed, rather than in advance:
... test("UserView should show user", function() { var view = new UserView(new User({ id: 1 })); view.render(); this.server.respond("GET", "/users/1", [ 200, { "Content-Type": "application/json" }, JSON.stringify({id: 1, name: "John Tester"}) ] ); equal(view.$(".name"), "John Tester"); });
This is more concise and keeps the response mocking closer to where it is actually been used. It won’t work if you’re trying to test a couple of different calls that are made at the same time though – in that case, you’ll have to resort back to defining the responses in advance.
It’s also worth noting that whilst in real-life any Ajax call to the back-end would be asynchronous, when we’re mocking the back-end using Sinon, everything stays synchronous. In the example above, this means that any callbacks in the UserView
that handle the XHR response will be invoked when this.server.respond
is called.
The consequence of all this is that the test does not need to be asynchronous. This is a Good Thing. We’ll go more into this later.
Debugging Server Calls
Sometimes your mocked-up routes don’t match and it can be difficult to figure out why not – by default, nothing will happen and you’re given no feedback. Alternately, if an error occurs in your own code that is called-back by a Sinon server.respond
call, Sinon will give you no indication that a problem occurred.
Fortunately Sinon provides a means to log errors – but you have to set it up manually. Add something like this to your test setup code:
... sinon.log = function(message) { console.log("Sinon received the log message '" + message + "'"); };
This will log all calls that come into the mock server and what the response is. It’ll also log any exceptions that might occur when the mock server invokes a callback in your code. The log output can be a little noisy, but it’s infinitely preferable to nothing at all.
Selective Server Mocking
Sometimes you don’t want to mock up all calls to the backend for certain paths. For example, you might want requests for templates to still go through to the actual backend. In this case you can define a filter in your test config:
sinon.FakeXMLHttpRequest.useFilters = true; sinon.FakeXMLHttpRequest.addFilter( function(method, url, async, username, password) { return url.indexOf('/my_templates/') === 0; } );
That said, having to accomodate asynchronous template loading can significantly complicate your code, which is a Bad Thing. Again, I’ll come back to this later.
How to mock the clock
Another important thing to control in your unit tests is the clock. If you don’t, time-dependent behaviours become difficult (if not impossible) to test reliably.
Sinon has a setting that allows you to globally mock the clock. You can fix the time, and progressively move it forward at your command. Consider our earlier example that activated a button if a date had been entered – what if the button activation was animated over one second and thus didn’t happen immediately? To test it, we’d ensure that we had the following in our global test config:
... sinon.config.useFakeTimers = true; ...
Then write our test like this:
... test("next button is disabled unless date is set", function() { var view = new SomeViewBeingTested({ model: new ModelForTheView() }); view.render(); // 'Next' button should be disabled by default var $nextButton = this.view.$("button.submit"); ok($nextButton.hasClass("disabled")); // Now set the date var $date = this.view.$("#date"); $date.val("July 28 2012"); $date.trigger(jQuery.Event("keyup")); // Move the clock forward one second this.clock.tick(1000); // And check that the next button has been enabled ok(!$nextButton.hasClass("disabled")); });
Unfortunately if you are testing asynchronous functionality and thus need to write an asynchronous QUnit test, it doesn’t play well with Sinon’s mock clock. Calls to the start()
method – which we use to tell QUnit when an asynchronous test has finished – will get blocked. This means you can’t mock the clock globally.
Consider an example where you’re testing an asynchronous render()
method (which frameworks like Backbone.LayoutManager use). You can still mock the clock up on a test-by-test basis, but you have to make sure you unmock it before you call start()
.
Say for example that you want to test a view that displays the current time. If it was an asynchronous test, it’d need to look something like this:
... asyncTest("TimeView should show current time", function() { var clock = sinon.useFakeTimers(); // Will set the clock to epoch var view = new TimeView(); view.render().done(function() { equal(view.$("time").text(), "1970-01-01T00:00:00"); clock.restore(); // Need to do this before we call start() start(); });
Note that these tests are over-simplifying things a bit – if the test fails, the clock won’t get unmocked. This means subsequent tests might try and mock the same thing twice, which will cause Sinon to fail. Alternately, your global mock may hang around for latter tests to use unwittingly, causing all sorts of bewildering errors.
To make matters worse, if you’re using something like require.js to load your tests asynchronously, the test execution order can differ from run to run, meaning that the failure can occur intermittently.
Consequently we have to also ensure that we unmock the clock at the end of every test (or any other global variables that you’ve mocked up). There’s a couple of ways to do this, but probably the most effective is to use the Sinon Sandbox. I won’t go into the details here.
Strategies for Simplifying Testing
As you’ve probably noticed, testing Backbone Views can get pretty tricky. Fortunately there are a couple of strategies that you can use to control this complexity.
Reducing Asynchronicity
The biggest contributor to test complexity is asynchronicity. The good news is that if you mock server calls and the clock using Sinon, you’re removing two of the main sources of asynchronicity. However, you have to ensure that ALL such calls being mocked up.
For example, if you’re still trying to load templates asynchronously, then your tests will still have to be asynchronous. However, it’s often the case that this loading is only asynchronous in development, because in production builds it’s common to precompile templates into Javascript, which will mean they are loaded synchronously. Having all of that complexity just for the sake of asynchronous template loading in your development environment seems excessive.
Consequently, I’d argue that it might be easier to just make all template loading synchronous, and thus remove this source of asynchronicity in your codebase. The tradeoff will be that during development and unit testing, templates won’t be loaded asynchronously and will – in theory – be slower. However, this probably won’t be a big deal and in my experience isn’t even noticeable.
Controlled Bootstrapping
Another thing you can do to simplify your unit testing is to ensure that your app does not do anything involving XHR calls, the DOM, or the router as your JS files get loaded. Instead, you should define a function that kicks off these activities, and only invoke it if and when you want to.
Normally, the actual index.html
file of your application will call this function, whereas the HTML file for your tests will not.
The consequence of this is that your application doesn’t bootstrap in your tests. This makes it easier to control the test environment. Nothing gets written to the DOM that you don’t want, no network calls get made before you get a chance to mock things up, no router gets started. Instead, all that happens is that your classes and functions get defined (your units), so that your unit tests can then test them. They are only unit tests, after all, not integration tests.
Structuring your app in this way can be a little tricky if you’re using a framework like require.js, which provides an easy means of bootstrapping your app as soon as it gets loaded. In that case, you have to circumvent Require’s preferred bootstrap mechanism and do it yourself.
Here’s an example: normally, require.js encourages you to bootstrap your entire app in one hit by putting something like this in your index.html
:
... <script type="text/javascript" src="assets/js/libs/require.js" data-main="app/config"></script>...
where app/config.js
usually loads some sort of application bootstrap module (amongst other things) – which in this case we’ll call app
:
require.config({ deps: ["app"], ... // Configure other dependencies like Backbone, Underscore, etc });
and the app.js
module boostraps itself as it is loaded. For example, at the very least it’d probably start a Backbone router:
define(["underscore", "backbone", "router"], function(_, Backbone, Router) { new Router(); Backbone.history.start(); } );
To postpone bootstrapping, we need to make it that the app
module doesn’t actually do anything straight away. Instead, we’ll have it return a function that can be used later to bootstrap the app:
define(["underscore", "backbone", "router"], function(_, Backbone, Router) { return function() { new Router(); Backbone.history.start(); }; } );
The module could alternately return an object with a start
method, but the simplest thing is just to return a function. Either way, we now have control over if and when the app actually starts. This means that in your index.html
, you can do this:
... <script type="text/javascript" src="assets/js/libs/require.js"></script> ... <script type="text/javascript">// <![CDATA[ require(["app/config"], function() { require(["app"], function(app) { app(); }); }); </script> ...
Note that we have omitted the data-main attribute and instead require
d app/config
ourselves, then waited until the app
dependency is ready.
In the HTML file for your QUnit tests (which in this example I will assume is in the same directory as your index.html
) you can refrain from invoking the app
module function, but still launch your tests:
... <script type="text/javascript" src="assets/js/libs/require.js"/> ... <script type="text/javascript"> require(["app/config"], function() { // Do any global test config here require(["test/my_first_test.js", "test/my_second_test.js"]); }); </script> ...
Consequently, no router is running whilst the tests are executing. Note also that before we launch the tests we have the opportunity to do any test-related configuration.
If you’re using something like r.js to compile your application into a single Javascript file in production, the bootstrap process is slightly different, because the compiled JS will automatically and synchronously load your app config module. Consequently, your production index.html can be simplified to this:
... <script type="text/javascript" src="dist/release/require.js"/> ... <script type="text/javascript"> require(["app"], function(app) { app(); }); </script> ...
where dist/release/require.js
is your compiled JS file.
Thin Views, Fat Models
Hopefully by now you’re appreciating the trickiness of testing Backbone Views. Another good way to minimise the amount of view testing that you have to do is to move as much business logic into your models as you can, and test it there. That way you don’t have to worry about querying and manipulating the DOM.
For complex UIs it even might be worth consider introducing view-models – models that represents a particular state of a view. View-models are view-specific and are particularly useful for non-trivial visualisations of data – for example, sorting, grouping and filtering.
This post has gone on for long enough so I’ll leave examples to a future posting.
Let’s Wrap This Up
In this post I’ve covered lessons learnt from over a year of testing Backbone views. I’ve shown that you need to consider how you’re going to interact with the DOM, how you’re going to mock up server call, and how you’re going to mock up the clock. To reduce test complexity, I’ve proposed eliminating unnecessary asynchronicity, separating your application startup from your module loading, and moving logic to models where possible.
However, the fact remains that without constant vigilance – especially when mocking globally accessible entities – you can rapidly end up encountering pretty bewildering errors when testing Backbone Views. I’m hoping that frameworks like Angular that have been designed with testing in mind provide a more viable approach to developing and testing single-page applications.
No Comments