Building testable applications with Backbone.js

Piecing libraries together to quickly produce a result can be one thing; building maintainable applications and test suites around them however, can be quite another.

The former approach leads in spending endless hours of frustration and mismanagement, while the latter in the freedom or mastery of “bending” tools and abstractions to serve business requirements in a precise way, an ability that can be discerned in skilled engineers.

While the quick approach can be our only choice in certain situations, one will eventually need to incorporate testing in the development process, sooner or later, if there's any hope of achieving anything beyond a mere prototype.

Testing goes hand-in-hand with refactoring however, so basic code organization skills along with an understanding of any frameworks our code is based upon, are essential to the process.

The discipline of testing has many facets and for the purposes of this post I have included an overview of inheritance in Backbone with a series of code organization and testing recipes that have been proved to me to be really useful.

Let's jump right in and have a look at how the inheritance model is built in Backbone.js base objects.

Inheritance in Backbone

Backbone provides us with a powerful way to extend its base objects. This section assumes knowledge of objects, prototypes functions and scopes in JavaScript.

As we will see later on when we'll be using a test framework to spy on methods expecting certain results back, without clear understanding of this behaviour of the framework we would be unable to test important parts of our code.

We will focus on the following piece of code for the remainder of this post:

var Widget = Backbone.View.extend({
    initialize: function (opt) {
        this.listenTo(this.model, 'change', this.render);
    },
    someMethod: function () {
        console.log('I am a method');
        return this;
    },
    render: function () {
        return this;
    }
});
var widget = new Widget({
    el: '#element',
    model: widgetModel
})

Keeping everything in mind as two distinct phases, type creation and instantiation respectively, occuring at different moments in time, helps us understand how inheritance in Backbone works.

Creating the type

Having a look at Backbone's source code we can see that objects passed as first argument in extend, get mixed-in with the prototype of the resulting object.

Let's examine what happens when we call extend from a Backbone object in more detail (view the source code for this section here):

  1. Resulting object stores the original base constructor function, without calling it.
  2. Static properties are mixed with the resulting object, if supplied.
  3. Prototype chain is setup using a surrogate object.
  4. Prototype properties are mixed with the resulting object, if supplied.
  5. A reference pointing to the parent's prototype is stored in __super__ and the resulting object is returned.

Widget results in a constructor which—once invoked in the next phase—will immediately call Backbone.View (the base constructor), and carries in its prototype all of our additional properties.

Creating the instance

Here's what's happening inside the Backbone.View base constructor (view the source code for this section here):

  1. A unique ID is generated for the resulting object.
  2. The resulting object gets assigned a series of specific instance properties or methods (model, collection, el, etc..) , if these are specified.
  3. DOM setup takes place.
  4. initialize gets called and the resulting object is returned.

Of this second series of steps, our focus will be mainly placed on steps 2 and 4, which are common to all Backbone base objects (not just the View.)

It can be useful to think this phase happening when the new keyword in our code is encountered, as this is where the constructor is called.

Code organization

We should now look at some simple ways to organize your code, that can make it lend itself to the power of our test suits, with ease.

Module management

I have incorporated CommonJS into my workflow and gave browserify a try during this summer. This proved to be a great choice in many situations.

As much as I like the AMD pattern and used it in many projects, I have to admit that the CommonJS syntax is way more terse and can be customized to suit our needs easily.

There's also the additional benefit that we can dissect pieces of our code using the Node REPL and its libraries directly, which can make the process of testing really efficient.

Having a consistent way of exposure across all modules makes code more predictable resulting in a clean API. Remember, the state our code is in will eventually be reflected back to us once we begin writing tests for it. Sorting these details out from the beginning goes a long way.

// PageControlView.js
var PageControlView = Backbone.View.extend({});
module.exports = function (opt) {
    return new PageControlView(opt);
}
module.exports.getClass = function () {
    return PageControlView;
}

// SomeOtherModule.js
var pageControlView = require('./js/views/pageControl')({ prop: prop });
var PageControlView = require('./js/views/pageControl').getClass();

The getClass() method is ofcourse syntactic sugar for returning a reference to the constructor, which we'll be needing. Inlining instantiation this way can be really handy as well.

CommonJS modules can be bundled together into a single JavaScript file for use by the client-side using the browserify node module.

If you use the Underscore templates, you will also going to need the brfs transform, which browserify can process your modules with, to compile your templates into JavaScript code that can then be included as a CommonJS module.

We can then include templates in our modules like this,

var fs = require('fs');
var pagesTmp = fs.readFileSync(__dirname + '/../templates/pages.html', 'utf8');

and browserify will inline the compiled JavaScript, living the code in a form that will still work in the console at the same time, which as we'll see will be very handy.

Here's how to install browserify with brfs and an example of bundling your code using them:

npm install -g browserify # Install browserify globally
npm install --save-dev brfs # Install brfs
browserify -t brfs js/main.js > bundle.js # Create your bundle

Grunt plugins also exist that can make the bundling process automatic.

Setting up an event-bus

Writing maintanable code also means having the freedom to switch between different implementations of modules without breaking the code or need to rewrite a large part of it.

The way our modules interact with each other play the most significant role in this case. Fortunately, Backbone is heavily based on events and message passing and we can utilize this behaviour right away.

A module that uses events, does not need to be tied to the behaviour of another. It can simply notify all of its subscribers that something significant has happened and let them respond to it on their own.

A quick solution is to create an event-bus, a simplified pub/sub implementation, that will be passed as dependency in each module. That way we can trigger events from and subscribe to, our event-bus. Here's an example:

var eventbus = _.extend({}, Backbone.Events);

var SomeView = Backbone.View.extend({
    initialize: function(opt) {
        this.eventbus = opt.eventbus;
        this.listenTo(this.eventbus, 'widget:closed', this.doSmth);
    },
    events: {
        'click #submit': 'clicked'
    },
    clicked: function() {
        this.eventbus.trigger('someview:submit:clicked');
    },
    doSmth: function() {
        // ...
    }
});

We don't have to worry about the behaviour of other objects anymore, as each module can exist on its own.

A whole lot can be written about programming with events, about their correct use and their flow or direction but these are beyond the scope of this post.

Writing the tests

Having tried a couple combinations of tools and test frameworks, I ended up using the Mocha-Sinon-Chai stack and test my code from the command-line.

npm install -g mocha 
npm install --save-dev sinon chai [email protected]

Sinon is used for things like mocks, spies and stubs, Chai equips us with any assertion style one might need, and Mocha is used as a test-runner that can be configured to suit every need.

My focus is placed more on the way data is handled by the code and the way each module interacts with each other, as this is the place where most of my bugs occur most of the time.

A DOM implementation will be required however, should we decide to test our Backbone Views, and as this is normally provided by the browser, we would need to an implementation that is “digestible” by node, the jsdom.

Spying on Backbone events

Sinon provides us with spies, a very useful feature, that helps us learn things like how many times the method we're spying on has been called during a test, with what arguments etc.

Spies, according to the docs, work by wrapping our method in another function and by storing a reference to the original one so that we can restore it later.

The following is a trivial example, using the widget object from backbone inheritance section, which passes our test:

var widget = new Widget();
var render = sinon.spy(widget, 'render');
widget.render();
assert.isTrue(render.spy.called);

Let's now try to trigger a change event in widget's associated model:

var render = sinon.spy(widget, 'render');
widget.model.trigger('change');
assert.isTrue(render.spy.called);

No matter how many times we may try running this last test however, it will always fail to pass our assertion.

If we take a look back at the definition of Widget, we can see that listenTo is called inside initialize:

var Widget = Backbone.View.extend({
    initialize: function (opt) {
        this.listenTo(this.model, 'change', this.render);
    },
    // ...

Sinon fails to wrap the callback function, as this occurs inside initialize, which is called by the constructor (at instantiation) before the wrapping takes place. As a result Backbone will keep calling the original function and not the spy we have created.

As functions in JavaScript are first-class objects, passed by reference, Sinon may wrap the render method succesfully, but the reference to the originl function has been set into memory and used by Backbone events code and will not be affected.

One solution would be to redefine initialize in our tests, calling sinon spy inside it, but I prefer the following which is even more terse:

var Widget = require('../js/views/widget').getClass();
var spy = sinon.spy(Widget.prototype, 'render');
var widget = new Widget({
    model: require('../js/model/model')() 
});
widget.model.trigger('change');
assert.isTrue(spy.called);

We create a reference to the constructor and use the Sinon spy to wrap Widget's prototype render function, before instantiating our new widget object. Our test now succesfully passes our assertion!

Spying on the Widget.prototype.render method before instantiation from the very beginning, ensures that this will be the reference Backbone will be using for our function.

We would be unable to figure all that out however, had we not taken the time to understand the internals of Backbone and how its inheritance model works.

These last two snippets also demonstrate the versality of the module pattern mentioned in the module management section: instantiating with a single require statement in the case of the model, and requiring the constructor function, in the case of Widget.

Closing thoughts

We have examined several ways one can write testable code with Backbone.js and discussed couple of best practices that can make the process easier.

Deep understanding of the dynamic nature of JavaScript along with knowledge on how frameworks we use work, is of vast importance in writing tests and help us discover bugs easier.

Testing leads in building maintainable applications and it immediately puts us in the right path towards accomplishing exactly that.

Writing testable code on top of the concise, well-written code of Backbone.js can help us boost our development workflow and productivity and be ready to tackle any challenge that might arise.