Thursday, September 22, 2011

Using Jasmine Spies with Backbone.js

‹prev | My Chain | next›

Over the past week or so, I have made some pretty hefty changes to my Backbone.js calendar application. I am optimistic that, by keeping my jasmine test suite green, things will still work in the live application.

It does not, however, take long to find a problem while clicking around. When I edit calendar appointments, I am greeted with this message:
Actually, I expected that one. That is my clever default error handling on save. The underlying cause, as the Javascript console indicates, is that I have not yet implemented the appointment PUT message on the backend:


After adopting the POST method to PUT in the backend, I am still seeing errors. These turn out to be related to the save() method in my Backbone app:
        var Appointment = Backbone.Model.extend({
          urlRoot : '/appointments',
          save: function(attributes, options) {
            attributes || (attributes = {});
            attributes['headers'] = {'If-Match': this.get("rev")};
            Backbone.Model.prototype.save.call(this, attributes, options);
          },
          // ...
        }):
If you look closely, you might notice that I am putting the the revision headers (required by CouchDB for updates) in the model attributes. I should be putting that in the options.

The fix is easy enough, but this seems like an opportune time to practice my Backbone BDD skills. So I add a new spec to my jasmine suite. I already have specs verifying most of the update life-cycle. Here, I just want to verify that save is called with an If-Match header.

I am not quite sure how to do this, but I think it will involve a jasmine spy. So I spy on the save method and check that the second argument contains the expected headers:
  describe("updating an appointment", function (){
    it("sets CouchDB revision headers", function() {
      var spy = spyOn(Backbone.Model.prototype, 'save').andCallThrough();
      var appointment = calendar.appointments.at(0);

      appointment.save({title: "Changed"});

      expect(spy.mostRecentCall.args[1])
        .toEqual({headers: { 'If-Match': '1-2345' }});
    });

    // ...
  });
That successfully gives me a red test:
A red test is a good thing. It means that I have successfully written a test that describes the bug that I have in my application. Now I can enter the change-the-message or make-it-pass BDD cycle.

To change the message, I change the save() method. Instead of putting the headers in the model attributes, now I store them in the options where they belong:
        var Appointment = Backbone.Model.extend({
          urlRoot : '/appointments',
          initialize: function(attributes) { this.id = attributes['_id']; },
          save: function(attributes, options) {
            options || (options = {});
            options['headers'] = {'If-Match': this.get("rev")};
            Backbone.Model.prototype.save.call(this, attributes, options);
          },
          // ...
        });
Checking my spec, I now see:
Hrm... Well, at least the message has changed. Actually, upon closer inspection, that does not seem like a big problem. It looks as though the options to save() pick up success and error callbacks. So instead of checking the entire second argument to save(), I only check the headers attribute of the second argument:
  describe("updating an appointment", function (){
    it("sets CouchDB revision headers", function() {
      var spy = spyOn(Backbone.Model.prototype, 'save').andCallThrough();
      var appointment = calendar.appointments.at(0);

      appointment.save({title: "Changed"});

      expect(spy.mostRecentCall.args[1].headers)
        .toEqual({ 'If-Match': '1-2345' });
    });
    // ...
  });
With that, I am green!
Nice. And, after a few quick clicks around the real app, I am satisfied that everything works in real life too!

That is something of a weak test (easy to break if functionality changes or Backbone changes). Still it is good to know that BDDing new Backbone features is not only possible, but pretty darn easy. Even more impressive is that I made massive changes to the application over the past week without breaking a thing. Because I religiously ran my jasmine test suite with each change, I weathered change in solid shape.

And now I have one more thing that I won't break.


Day #141

No comments:

Post a Comment