Chapter 7. Testing Backbone.js Applications

Testing Backbone applications is no different than testing any other application; you are still going to drive your code from your specs, except that Backbone is already leveraging a lot of functionality for you, for free. So expect to write less code, and consequently less specs.

Backbone is a micro framework designed to give web applications just enough structure to allow them to grow. It provides four base abstractions:

  • Model: It provides a key-value store for the application data along with custom events
  • Collection: It provides a rich enumerable API
  • View: It creates the interface building blocks
  • Router: It provides methods for client-side routing

We will see some common testing scenarios when dealing with each type of abstraction, and some common mistakes people make while creating these specs.

The Backbone model

They are the real backbone of Backbone, they are the abstractions on which we build the business logic, they hold the data and are responsible for performing validations and synchronization with a remote server; they are the Backbone models.

Although we are not using Backbone on our sample application, we already have some well-defined models, both the Stock and Investment objects. But before we dig in how we could refactor them to become Backbone models, we need first to get a little bit of understanding on how they work.

Declaring a new model

To create a new model object we need first to extend it from the base Backbone model. To make matters more interesting, we are going to rewrite the entire Stock spec, by expecting the Stock to be a Backbone model.

On the Stock spec we could write the following acceptance criterion, that although not business related, guarantees that this model will inherit all of the model's functionalities:

describe("Stock", function() {
  var stock;

  beforeEach(function() {
    stock = new Stock();
  });

  it("should be a Backbone.Model", function() {
    expect(stock).toEqual(jasmine.any(Backbone.Model));
  });
});

To implement this new model we need to use the Backbone.Model.extend function in the Stock source file:

(function (Backbone) {
  var Stock = Backbone.Model.extend();
  this.Stock = Stock;
})(Backbone);

And we are done! The Stock is now a fully featured Backbone model with all of its niceness.

But now that we are done, what can we do with it?

The model attributes

At the core of a Backbone model is a key-value store that should be used to hold all of the model's data. And you can access it by a very simple API of getter and setter functions. Given here is a code snippet of how it works for the previously declared model:

var stock = new Stock();
stock.set('sharePrice', 10)
stock.get('sharePrice') // 10

You can also pass objects to the set function, in order to set multiple parameters:

stock.set({
  sharePrice: 20,
  symbol: 'AOUE'
});

And still be able to get each attribute individually:

stock.get('sharePrice') // 20
stock.get('symbol') // AOUE

You can even pass all of the Model's attributes on its instantiation, and the getter method would still work for each attribute individually:

var stock = new Stock({
  sharePrice: 20,
  symbol: 'AOUE'
});

Now that you understand how the model attributes work, let's revisit a simple test case of the Stock spec (already ported to Backbone):

beforeEach(function() {
  stock = new Stock({
    sharePrice: 10
  });
});

it("should have a share price", function() {
  expect(stock.get('sharePrice')).toEqual(10);
});

Although this scenario was useful to test a piece of code previously, it has now become obsolete. We are basically testing a Backbone functionality, something that is already well tested by the Backbone library itself, so you can safely remove that spec and still be confident that the model attributes are working (as long as you have the spec to test that it is a Backbone model).

Good! So far, Backbone has helped us write both less code and fewer specs.

Tip

Although you can write a spec code to everything, try not to repeat yourself (by rewriting other people's specs) and write specs that cover a piece of code that you have actually written.

Default attribute values

Back to the model attributes, we are going to explore a Backbone functionality that does require some testing from our part, but that is because we are also going to write some code to make it work. It is a very simple functionality to set default model values on instantiation.

Still at the Stock example, let's add another acceptance criterion that it should have a default share price value of zero:

describe("Stock", function() {
  var stock;

  beforeEach(function() {
    stock = new Stock({ symbol: 'AOUE' });
  });

  it("should have a default share price of zero", function() {
    expect(stock.get('sharePrice')).toEqual(0);
  });
});

We have seen standard Backbone attribute handling previously. Notice that we are not setting the share price value at any time on the spec, so it is up to the Stock code to make it happen.

And here comes another Backbone functionality, normally we would set this on a constructor function, but Backbone deals with that for us, all we have to do is declare a defaults object in the Stock definition, providing the default value for each of the model's attributes. Let's see how it works:

var Stock = Backbone.Model.extend({
  defaults: {
    'sharePrice': 0
  }
});

Simple and clean.

Events

As we have seen in Chapter 3, Testing Frontend Code, the Observable pattern was a great solution to perform the integration between two Views, while reducing coupling. In Backbone, every one of its four abstractions is built on a shared event infrastructure, making all of them Observable objects by default. In models, it is possible to observe changes in the whole model or individual attributes, be notified when it syncs with a backend (as we will see later) and even possible to create custom events. If you are familiar with how to listen for events on jQuery, using Backbone functions will feel right at home.

Again, let's see some code to understand how it works:

var stock = new Stock();
stock.on('change:sharePrice', function () {
  alert('it has changed!);
});
stock.set({ 'sharePrice': 30 })

We are using the same Stock model defined earlier. First, we add an observer through the on function, to listen for changes of the share price value on the stock instance. You can see that we are passing the event name (change:sharePrice) and a function to be called back once the event happens.

Then later, we change the value of the share price attribute. Behind the scenes, Backbone notices the change and notifies all listening observers, showing the alert message (it has changed) on your browser.

This is great to keep interfaces and models in sync as we will see later on. But how can we harness this functionality to any good purpose inside the model itself?

Remember the Investment object? After converting it to a Backbone model, we can rewrite one of its specs to demonstrate a test case involving model events.

Here is a good candidate involving the roi attribute:

describe("when its stock share price valorizes", function() {
  beforeEach(function() {
    stock.set('sharePrice', 40);
  });

  it("should have a positive return of investment", function() {
    expect(investment.get('roi')).toEqual(1);
  });
});

The roi attribute is a so called virtual attribute, as it is a result of a calculation between two other attributes. Previously, we had chosen to implement it like a function, and we still could, but doing so we would lose the benefit of having a homogeneous interface between all of the model attributes, and besides, roi being a regular attribute, others can also observe for changes on it as well.

Recapitulating, an Investment return of investment is the ratio between the paid share price and its current value. We could create a function to calculate it like:

function calculateROI () {
  var sharePrice = this.get('sharePrice'),
  var stockSharePrice = this.get('stock').get('sharePrice'),
  this.set('roi', (stockSharePrice - sharePrice) / sharePrice);
}

By now you probably already know how to make the spec pass. We want to calculate the return of investment every time an investment stock changes its share price.

To do that inside the Investment object, we need to add an observer to the stock attribute once an investment is created:

var Investment = Backbone.Model.extend({
  initialize: function () {
    this.get('stock').on('change:sharePrice', calculateROI, this);
  }
});

Done! We had to pass another configuration during the definition of Investment to specify an initialize function. It acts like a constructor, being called once a model has been instantiated.

Obviously this implementation is not complete, since the stock attribute itself might change to a different stock, requiring the rebinding of the change observer. But I'll leave that as an exercise for you.

We will dig more into Backbone events in this chapter, but for a complete reference, be sure to check the official documentation available at http://backbonejs.org/#Events.

Sync and AJAX requests

A model wouldn't be a model without some sort of mechanism to allow it to be read or saved to a server. And as you have guessed, this piece of implementation is about to get much simpler for you.

By default, Backbone comes with support for backend servers that implement the REST standard, and all that is left for you to do is to configure an end point for it to make requests and it automatically fetches, updates, creates, and delete models.

For now, the only piece of application that depends on a backend server is the Stock model with its fetch function. Let's see what Backbone has to offer on this matter.

Here is the original spec implementation with a few tweaks to make it simpler and compatible with Backbone:

describe("Stock", function() {
  var stock;

  beforeEach(function() {
    stock = new Stock({ symbol: 'AOUE' });
  });

  describe("when fetched", function() {
    var fakeServer;

    beforeEach(function() {
      fakeServer = sinon.fakeServer.create();
      fakeServer.respondWith('/stocks/AOUE',
                             '{ "sharePrice": 20.13 }'
                             );

      stock.fetch();

      fakeServer.respond();
    });

    afterEach(function() {
      fakeServer.restore();
    });

    it("should update its share price", function() {
      expect(stock.get('sharePrice')).toEqual(20.13);
    });
  });
});

This is the implementation using Sinon.JS Fake server seen in Chapter 6, Light Speed Unit Testing. Before we had to implement this fetch function by ourselves, but with Backbone, all we have to do is set up two configurations on the Stock definition:

var Stock = Backbone.Model.extend({
  idAttribute: 'symbol',
  urlRoot: '/stocks'
});

These attributes are:

  • urlRoot: This indicates the root URL that Backbone needs to perform AJAX requests into
  • idAttribute: This indicates which of the model's attribute it must use as an ID while making the AJAX request

Once again we are done! The spec should be passing and everyone in the room should be happy!

But that spec we have written is appearing to show its age, it is testing so much more than the code we have written. Although it is not a bad thing, we can make things simpler by trusting that Backbone is doing a correct implementation of its fetch function (given that we supply the right parameters) and rewrite the spec, now that we are familiar with the Backbone API:

describe("Stock", function() {
  var stock;

  beforeEach(function() {
    stock = new Stock({ symbol: 'AOUE' });
  });

  it("should allow fetching its information", function() {
    expect(stock.idAttribute).toEqual('symbol'),
    expect(stock.urlRoot).toEqual('/stocks'),
  });  
});

We have replaced the whole when fetched describe function by a single spec with two assertions, while still assuring that the fetch function works.

Once again, less code and fewer specs. But take notice that we could have left the old spec implementation. It does provide a little more confidence, since you could (in theory) write both the implementation and the spec wrongly, such as a mistype of urlRoot as urlroot on both files.

It is a tradeoff between simplicity and a little more security. It is your call when to use each approach.

There is much more to Backbone sync, such as support for Local Storage or XML; be sure to check the full documentation available at http://backbonejs.org/#Sync.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset