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

No comments:

Post a Comment