Showing posts with label sinonjs. Show all posts
Showing posts with label sinonjs. Show all posts

Sunday, November 27, 2011

BDDing Backbone with Sinon.js Fake Servers

‹prev | My Chain | next›

Up today, I continue my efforts to BDD a simple appointment list view in my calendar Backbone.js application. The only significant remaining feature is more of a bug—when switching from month view to list view, the list view is retaining the collection filter that only requests appointment from a single month.

In jasmine-speak, I might say:
    it("requests all appointments (forgets previous filter)");
Since I am faking the request to the server, how I go about expressing my expectations is going to be a little tricky. Tricky, perhaps, but I do not have many options. I am forced to spy on the AJAX request that goes out. Or maybe not...

In my setup, I am using sinon.js to stub out AJAX requests to anything under the "/appointments" URL space:
  describe("list view", function() {
    beforeEach(function() {
      // ...
      server.respondWith('GET', /\/appointments/,
         [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
    it("requests all appointments (forgets previous filter)");
  });
If I change my setup to only respond when the "/appointments" URL is called (i.e. without filtering query parameters), then I ought get the desired result:
  describe("list view", function() {
    beforeEach(function() {
      // ...
      server.respondWith('GET', '/appointments',
         [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
    it("requests all appointments (forgets previous filter)");
  });
In fact, I do get my desired result because my earlier list view tests fail:

To make those pass again, I need my application view to clear my collection's date and for the collection to not pass a date query parameter when the date is not set:
    var Application = Backbone.View.extend({
      // ...
      setListView: function() {
        this.view = 'list';
        this.collection.setDate();
        this.collection.fetch();
        return this.render();
      },
      // ...
    });

    var Appointments = Backbone.Collection.extend({
      // ...
      fetch: function(options) {
        options || (options = {});

        var data = (options.data || {});
        if (this.date) options.data = {date: this.date};

        // ...
        return Backbone.Collection.prototype.fetch.call(this, options);
      },
      // ...
    });
With that, I have all of my tests passing:

And, all appointments are now shows on the list view:

With that out of the way, I am ready to tackle a few things that need verifying in Recipes with Backbone. Tomorrow. For now, I have much proof-reading ahead of me.



Day #218

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

Saturday, September 10, 2011

Watching Backbone Views Destroyed with Jasmine and Sinon

‹prev | My Chain | next›

Last night, I was able to mix the combination of expresso, jasmine, jasmine-jquery, jasmine gem and sinon.js into something that actually tested useful stuff in my Backbone.js application.

My expresso test for the backend does little more than generate fixtures for use with the jasmine tests. The jasmine-jquery package lets me load generated fixtures (and provides some nifty matchers to boot). I run everything under the jasmine gem so that I can load things up in a browser (fixture loading directly from the filesystem can be iffy). Lastly, sinon.js allows me to stub out AJAX requests, meaning I do not even need a backend to test. Beauty.

After a bit of clean-up, my jasmine test now reads as:
// Load before fiddling with XHR for stubbing responses
jasmine.getFixtures().preload('homepage.html');

describe("Home", function() {
  var server,
      couch_doc = { 
        "title": "Get Funky",
        "startDate": "2011-09-15",
        /* ... */
      },
      doc_list = {
        "total_rows": 1,
        "rows":[{"doc": couch_doc}]
      };

  beforeEach(function() {
    // stub XHR requests with sinon.js
    server = sinon.fakeServer.create();

    // load fixutre into memory (already preloaded before sinon.js)
    loadFixtures('homepage.html');

    // populate appointments for this month
    server.respondWith('GET', '/appointments', JSON.stringify(doc_list));
    server.respond();
  });

  afterEach(function() {
    // allow normal XHR requests to work again
    server.restore();
  });

  it("populates the calendar with appointments", function() {
    expect($('#2011-09-15')).toHaveText(/Get Funky/);
  });
});
Nice. I can load the calendar homepage, which then loads appointments from a fake backend, and ensure that the appointment for 15 September shows up on the calendar for that date. The fact that Backbone is responsible for loading and populating the appointments in the calendar UI is completely hidden here. So I am testing observed behavior and not implementation. Very cool.

Next up, I would like to verify that, when an appointment is removed from the backend, it is removed from the UI as well. Something along the lines of:

  it("removes appointments from UI when removed from the backend", function() {
    // Remove backbone model
    expect($('#2011-09-15')).not.toHaveText(/Funky/);
  });
I get this working by manually destroying the first (and only) Appointment model (stubbing out the XHR DELETE):

  it("removes appointments from UI when removed from the backend", function() {
    Appointments.models[0].destroy();

    server.respondWith('DELETE', '/appointments/42', '{"id":"42"}');
    server.respond();

    expect($('#2011-09-15')).not.toHaveText(/Funky/);
  });
And, with that, I have two passing Jasmine tests covering my app:
That is a good start, but, at this point, my test needs to look under the cover and tell the backbone model to delete itself. It would be better to click the "delete" icon on the page. That should have the same ultimate effect as deleting the model directly since the handleDelete() handler tells the model to destroy itself:
   window.AppointmentView = Backbone.View.extend({
    // ...
    handleDelete: function(e) {
      console.log("deleteClick");

      e.stopPropagation();
      this.model.destroy();
    },
    // ...
  });
So I rewrite my jasmine spec to click the delete icon:
  it("removes appointments from UI when removed from the backend", function() {
    // Appointments.models[0].destroy();
    $('.delete', '#2011-09-15').click();

    server.respondWith('DELETE', '/appointments/42', '{"id":"42"}');
    server.respond();

    expect($('#2011-09-15')).not.toHaveText(/Funky/);
  });
But, unfortunately, that does not seem to work:
Hrm...

To figure this out, I add a Chrome debugger statement to the test:
  it("removes appointments from UI when removed from the backend", function() {
    $('.delete', '#2011-09-15').click();

    server.respondWith('DELETE', '/appointments/42', '{"id":"42"}');
    server.respond();

    debugger;

    expect($('#2011-09-15')).not.toHaveText(/Funky/);
  });
When I drop into Chrome's Javasciript console now, I am unable to manually trigger the delete-click handler. In fact, there appear to be no handlers at all on the appointment:
If I manually add a handler, it does show up so it's not as if Chrome's event handler is buggy:
Bah! I have to call it a day at this point. I have my Jasmine tests verifying that my Backbone application can populate the calendar. I can even verify that removing an appointment will remove it from the UI. Hopefully tomorrow I will be able to solve the last little puzzle: simulating events.


Day #140

Friday, September 9, 2011

Stubbing Backbone Tests with Sinon.js

‹prev | My Chain | next›

Last night, I was able to take an HTML fixture from my Backbone.js application and run a simple jasmine test against it:
(the fixture loading is coming from jasmine-jquery)

That is not much of a test. It is more just an assertion of a configuration setting. To actually test my Backbone application, I am going to need to stub out the AJAX requests that Backbone makes. Even though my simple test passes, I see Backbone / AJAX errors in Chrome's Javascript console:
The current test:
describe("Home", function() {
  beforeEach(function() {
    loadFixtures('homepage.html');
  });

  it("uses the /appointments url-space", function () {
    var it = new window.Appointment;
    expect(it.urlRoot).toEqual("/appointments");
  });
});
If I can ensure that the AJAX call to /appointments returns and that I can control what it returns, I ought to be able to verify what the contents of the 15th will be:

  it("populates the calendar with appointments", function() {
    expect($('#2011-09-15')).toHaveText("Appt: Funk");
  });
So, before the fixture is loaded, I want to stub out calls to the /appointments namespace and return some dummy JSON data. Tonight, I am going to give sinon.js a try in this capacity. So first I grab the latest sinon.js:
wget http://sinonjs.org/releases/sinon-1.1.1.js -O spec/javascripts/helpers/sinon-1.1.1.js
There is no need to add this to spec/javascripts/support/jasmine.yml since it is already loaded by virtue of the default value for helpers (["helpers/**/*.js").
The actual response from my express.js (which is proxying a CouchDB backend) is:
➜  calendar git:(master) ✗ curl http://localhost:3000/appointments/
{"total_rows":7,"offset":0,"rows":[
{"id":"69bb0bd029c5ae5209168c96b0003125","key":"69bb0bd029c5ae5209168c96b0003125","value":{"rev":"1-4385f513d690af99b3bfaea2b08cb75c"},"doc":{"_id":"69bb0bd029c5ae5209168c96b0003125","_rev":"1-4385f513d690af99b3bfaea2b08cb75c","title":"Delete Me","description":"asdf","startDate":"2011-09-15"}},
/* ... */
]}
So maybe something like this will do:
jasmine.getFixtures().preload('homepage.html');

describe("Home", function() {
  beforeEach(function() {
    var server = sinon.fakeServer.create();
    server.respondWith(
      200,
      { "Content-Type": "application/json" },
      '{"total_rows":1,"offset":0,"rows":[' +
        '{"id":"42","key":"42","value":{"rev":"1-2345"},' +
        '"doc":{"_id":"42","_rev":"1-2345","title":"Funk","description":"asdf","startDate":"2011-09-15"}}' +
        ']}'
    );

    loadFixtures('homepage.html');

    server.respond();
  });

  it("populates the calendar with appointments", function() {
    expect($('#2011-09-15')).toHaveText("Funk");
  });
});
I preload my fixtures so that stubbing out XHR will not cause trouble with fixutre loading. Then I fake an XHR request.

Reloading, however, I am not seeing the fixture page populated:
That actually cuts down on XHR requests in the console, so I am fairly confident that sinon.js is doing something. Unfortunately, I am unable to get it to populate the Backbone collection. I am forced to call it a night here. Hopefully I can get this working correctly tomorrow.

Update: Figured it out. I was not sending back an array in server.respondWith(). The following works:
jasmine.getFixtures().preload('homepage.html');

describe("Home", function() {
  beforeEach(function() {
    var server = sinon.fakeServer.create();
    server.respondWith(
      [200,
      { "Content-Type": "application/json" },
      '{"total_rows":1,"offset":0,"rows":[' +
        '{"id":"42","key":"42","value":{"rev":"1-2345"},' +
        '"doc":{"_id":"42","_rev":"1-2345","title":"Funk","description":"asdf","startDate":"2011-09-15"}}' +
        ']}']
    );

    loadFixtures('homepage.html');

    server.respond();
    server.restore();
  });

  it("populates the calendar with appointments", function() {
    expect($('.appointment', '#2011-09-15')).toHaveText(/Funk/);
  });
});
This results in a passing test:
Yay!


Day #139