Friday, November 25, 2011

BDDing New Backbone.js Views with Jasmine

‹prev | My Chain | next›

I believe that I have more or less finished exploring testing Backbone.js with jasmine. Ready to tackle something new, I think I will try adding a different view to my calendar application. And what better way to add a new feature that driving it with BDD?

And lo! I know just the tool to BDD that feature. Looks like I am not quite done with Jasmine after all…

For my month view, I have just been stubbing (with sinon.js) server lookups to return a single appointment. For the list view, I think I ought to start by returning three appointments:
  describe("list view", function() {
    var couch_doc1 = {
          "_id": "1",
          "_rev": "1-1111",
          "title": "Appt 001",
          "startDate": "2011-10-01"
          // ...
        }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = {
          "total_rows": 3,
          "rows": [
            {"value": couch_doc2},
            {"value": couch_doc1},
            {"value": couch_doc3} ]
        };
My backend is, of course, CouchDB—hence the structure of the document list that will be returned. I am intentionally mucking with the order of the documents because I am anticipating sorting them in the front-end.

With the appointment documents ready, I write my spec setup, which does the usual of creating a new instance of my Backbone Cal application. It also stubs out server calls to return the document list from the main describe("list view", ...) block:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }

    beforeEach(function() {
      window.calendar = new Cal($('#calendar'));
      Backbone.history.navigate('#list', true);

      // populate appointments for this month
      server.respondWith('GET', /\/appointments/,
        [200, { "Content-Type": "application/json" }, JSON.stringify(doc_list)]);
      server.respond();
    });
  });
The only real difference between this setup and the setup for my month view is that I am navigating to '#list' rather than '#month'.

On that list page, I expect to see 3 appointments. Or, in jasmine-ese:
  describe("list view", function() {
    var couch_doc1 = { "_id": "1", /* ... */, "startDate": "2011-10-01" }
      , couch_doc2 = { "_id": "2", /* ... */, "startDate": "2011-11-25" }
      , couch_doc3 = { "_id": "3", /* ... */, "startDate": "2011-12-31" }
      , doc_list = { /* rows: couch_doc2, couch_doc1, couch_doc3 */ }

    beforeEach(function() { /* ... */ });

    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
When I load the new "list view" specs in the browser, I get the expected failure:


So my next question is, what do I need to do to change the failure message or make it pass? Simply asking the question makes me realize I have gotten ahead of myself. To do any of this, I am going to need a "list view". I do not even have a route that responds to such a thing, let alone a view.

So, I take a step back and mark this test pending (for now) and write a more appropriate first step:
  describe("list view", function() {
    var /* couch_doc1, couch_doc2, couch_doc3, doc_list */
    beforeEach(function() { /* ... */ });

    it("routes to the list view", function() {
      expect($('h1')).toHaveText(/Appointment List/);
    });

    xit("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
  });
With that, I have a smaller failure:


To be sure, I could make an even smaller first failure by testing the routing itself. I am jumping ahead of myself a little—hopefully I will not get burned. It turns out to require quite a few changes simply to get the new view to render. I have to add a new route, which calls a new method on my application view, which creates a new view type:
  var Application = Backbone.View.extend({
    // ...
    setListView: function() {
      this.view = 'list';
      return this.render();
    },
    render: function() {
      var view;

      if (this.view == 'list') {
        view = new CalendarList();
      }
      else {
        var date = this.collection.getDate();
        view = new CalendarMonth({date: date});
      }

      $(this.el).html(view.render().el);
    },
    // ...
  });

  var CalendarList = Backbone.View.extend({
    render: function() {
      $(this.el).html("<h2>Appointment List</h2>");
      return this;
    }
  });
After all of that, I have my first passing test:


That took a bit more effort than anticipated. I am pretty sure I have introduced a memory leak in there. I know that I am violating at least one Recipes with Backbone recipe ("Evented Routers"). But none of that matters: (1) get it done, (2) do it right, (3) optimize. I have reached #1 and that is good enough for now. I will worry about #2 and #3 another day if needed (and with the benefit of more tests).

For now, I would like to get my original test passing:
    it("lists all appointments", function() {
      expect($("li", "#appointment-list").length).toEqual(3);
    });
In the spirit of "get it done", I pull out an Underscore.js reduce():
    var CalendarList = Backbone.View.extend({
      initialize: function(options) {
        options.collection.bind('reset', this.render, this);
      },
      render: function() {
        $(this.el).html(
          '<h2>Appointment List</h2>' +
          '<ol id="appointment-list">' +
          this.collection.reduce(function(memo, appointment) {
            return memo + '<li>' + appointment.title + '</li>';
          }, "") +
          '<ol>'

        );
        return this;
      }
    });
To be sure, this is a very rudimentary listing of appointments. In the final version, I will certainly need to show more than a list of static titles. But it does enough to make my test pass:


Since it works, that will serve as a stopping point tonight.


Day #116

2 comments:

  1. One thing I notice is that you do not dispose of your views which might lead to zombie views.

    I mention this in this post http://www.thesoftwaresimpleton.com/blog/2011/11/13/backbone-js---lessons-learned/.

    Maybe the fact that you keep everything in memory means you do not have to dispose of them. It is something worth raising though

    ReplyDelete
  2. @Paul I think you're probably correct that I've got zombie views lurking about. Definitely something that I've got to address in the near future. Thanks!

    ReplyDelete