Testing Backbone views

Backbone views are responsible for presenting the state of the models to your user. Most commonly, they'll use the DOM to do this (equally I've written views that use Canvas, SVG or sound), and handling user interaction with that presentation. We'll focus on testing DOM views as they're most common.

The tasks of a Backbone view are simple, and we'll cover each in turn.

  1. present a representation of the model
  2. listen for events from user interaction
  3. pass on the commands that these events represent to the model layer

Presenting the model

A view wouldn't be doing a great job if it didn't give the user something to look at. Let's make sure our views do just that.

Backbone is liberal in its approach to views, leaving it up to you to decide how to render them. Step one is deciding on how to create some nice DOM elements. I use templates - specifically code-free templates like Mustache - as they're quick, testable and designer friendly. The examples in this article will reflect this setup.

Our first presentation test

Let's make sure the user has something to look at.

"test displays all of the items in the shopping cart": function() {
  var view = makeView(collection: new Item.Collection([
    {id:1,name:"Vase"},
    {id:2},
    {id:3}
  ]))
  view.render()
  assertHasCountCss(".item", view.el, 3)
  assertHasText("Vase", view.el)
}

This is a simple view test for a shopping cart. Its job is to render all of them items in a user's cart. We use a helper method to create the view with all dependencies, we render the view, and we use some assertions (these are some I've written for Sinon.js).

The two assertions are fairly simple, but do the job. We've ensured two key behaviours - we've rendered the correctly number of items (assertHasCountCss), and we've been able to render some of the data out of those items (assertHasText).

For me, it’s easy to over-test basic rendering in a templated view. We’re just doing a smoke test to ensure we get something sensible. Specifying more about the expected outcome would be comprehensive, but also fragile. More possibilities for CSS to change or for fields to be added or removed.

My mental checklist for basic view rendering is

This should provide coverage of the majority of your basic rendering logic.

Conditional sections

Views render based on model state, most often with a conditional segment.

"test checkout is enabled only when there are items": function() {
  var view = makeView(collection: new Item.Collection())
  view.render()
  assertHasntCss(".checkout[disabled]", view.el)
  view.collection.add([{id:1,name:"Vase"},{id:2},{id:3}])
  assertHasCss(".checkout[disabled]", view.el)
}

Tests for conditionals follow an obvious pattern:

Updating the view upon model changes

Continuing the theme, this is easy to test:

  "test I see the view update when state changes": function() {
    var view = makeView();
    item = view.collection.at(2);
    assertHasntCss(".finished[data-id=" + item.id + "]", view.el);
    item.set({
      state: "finished"
    });
    assertHasCss(".finished[data-id=" + item.id + "]", view.el);
  }

Just like most 'test something reacts correctly' tests, we've

With those three testing patterns you should be set for 90% of view presentational tasks.

Listen for events

Views handle user interaction with the DOM. How to test this is a common question in Javascript testing, so let's have a look.

  "test I can preview an item by clicking on it": function() {
    var view = makeView();
    var item = view.collection.models.random();
    var called = false;
    view.requestPreview = function() {
      called = true;
    }
    $(view.el).find(".item[data-id='" + item.id + "']").trigger("click");
    assertTrue(called);
  }

Here we've rolled out own mock to ensure the requestPreview method of our view is fired upon an item being clicked. We're using jQuery to fire the handler. This is a quick and effective way of testing - jQuery is extremely well tested so we can rely on it having done the event listener setup if we've done it declaratively via Backbone's events hash.

This is all there is to it for testing events. Render out the item which will handle interaction, fire the expected interaction via jQuery or your DOM library of choice, and make sure those methods are called! If you're bored of writing mocks, have a look at something like sinon.js - they have lots of useful mocking tools.

Update model layer

So here we're testing the view changes the model. Whenever you're doing something more complicated that a model.set(newData) you should create a model method. This is very similar to the advice you'll hear in writing Rails - keep your models fat and your controllers (in Backbone, View) skinny.

This is because models are where are the important rules and knowledge about what our system does for the user live. There are many ways of triggering modifications to the state of that system, but only certain modifications that are allowed, and certain things should always happen with those modifications.

If instead you implement all of these rules around modifying our domain state in a view, what happens when you add another view performing the same operation? If you said "copy the code there too", remove one cookie from your jar and GRIND IT INTO DUST. For that way lies madness - every time you change the domain rules, you know have two places to update. Miss one, and that's a ugly new bug a user will run into (not to mention two places to test).

Finally, model tests are simpler to write, being plain old Javascript rather than DOM or AJAX.

So that makes our job in testing view interaction with models super-simple! As we know they'll be calling a maximum of one model method per user interaction, you should already be saying to yourself "aha, a simple mocking job".

We'll take a look at an example from a test I wrote for Picklive's football game. We'd like to be able to add players to a team.

var playerOne;
var makeView = function() {
  return new RealPlayerView({
    collection: new Player.Collection([playerOne]),
    model: new Game
  })
};
TestCase("RealPlayerView",{
  "setUp": {
     playerOne = new Player();
  },
  "test I click on a player to add a player to the displayed entry": function() {
    var mock, view;
    view = makeView();
    view.model.canPick = function() {
      return true;
    };
    mock = sinon.mock(view.collection);
    mock.expects("add").once().withExactArgs(playerOne);
    view.$("#player-" + playerOne.id).eq(0).trigger("click");
    mock.verify();
  }
});

Here I've used a Sinon mock and a hand rolled stub to make the view think the model is in a certain state. In our setUp we've created a known player model which we're going to click and ensure they've been add to the collection.

Inside the add is where the validation etc will occur - all we're ensuring is that the view has done its job of passing the user's intention along to the model.

Roundup

So that's all I've got to say about testing Backbone views for now. View tests should definitely be in your testing toolbox.

If you found this useful, keep your eyes on the feed - they'll be plenty more articles on Backbone testing in the series. Tell me what you'd like to see @timruffles. Thanks!

Enjoy this? Subscribe to my RSS feed or follow me on Twitter.