Tuesday, November 22, 2011

Jasmine is Hard when Writing Bad Backbone.js Code

‹prev | My Chain | next›

By way of quick follow up to yesterday's Backbone.js testing post, an astute reader pointed out that I had failed to read the documentation on Backbone.history.navigate(). It seems that it takes two arguments—if the second is true, then the route being navigated will trigger.

Thus I can replace last night's default route:
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
      this.setMonth(month);
    },
    // ...
  });
Which can be replaced with a version that removes the explicit call to setMonth() and adds true as a second argument to navgiate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month, true);
    },
    // ...
  });
This works by virtue of a month-matcher route, which is fired after navigating to the month route in combination with the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    routes: {
     // ...
      "month/:date": "setMonth"
    },
    // ...
    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
And that works. When I open the root URL for my app, the default route kicks in, navigates to the current month and my appointments are on the calendar:


And if I remove the true second argument, my calendar now lacks appointments because nothing triggers the appropriate route:


The thing is, that my specs continue to pass:

This is because my tests inject a different, testing route and then manually does the route's job—setting the current date:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'), {
      defaultRoute: function() {
        console.log("[defaultRoute] NOP");
      }
    });

    window.calendar.application.setDate("2011-11");
    // ...
  });
Now, to a certain extent, I do not have much choice. I have to explicitly set the date in my tests otherwise my November based tests are going to fail once December rolls around. But on the other hand, how can I get my tests to fail when routing is broken?

Ah, but wait a second. I have been building on a number of assumptions that are probably incorrect. Instead of using Backbone.history.navigate() to move my application around, I have been manually setting window.location. I have already found that some things are easier if I use navigate(). Perhaps testing is similarly easier.

So, I remove the defaultRoute injection from my jasmine test setup:
  beforeEach(function() {
    // ...
    window.calendar = new Cal($('#calendar'));
    Backbone.history.loadUrl();
    // ...
  });
(I do need to read-add the manual Backbone.history.loadUrl() between runs per http://japhr.blogspot.com/2011/11/backbonejs-hash-tag-urls-with-jasmine.html)

Then, in my application code, I remove the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month);
    },

    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
With that, I am failing again:

Most importantly here, is not that I have failing specs, but that the console output includes logging statements for the default route being called, but not a subsequent logging message from setMonth(). If I re-add the true second argument to navigate():
  var Routes = Backbone.Router.extend({
    // ...
    _setDefault: function() {
      console.log("[setDefault]");
      var month = Helpers.to_iso8601(new Date).substr(0,7);
      Backbone.history.navigate('#month/' + month, true);
    },

    setMonth: function(date) {
      console.log("[setMonth] %s", date);
      this.application.setDate(date);
    }
  });
Then my Jasmine tests pass and my console logs output from both the default route and the subsequent setMonth() triggered route:


Nice. That was a big point of frustration in my Backbone testing efforts. As usual, had I been doing things idiomatically, life would have been much easier. Much thanks to Nick Gauthier (my Recipes with Backbone co-author) and Patrick (in last night's comments) for showing me the way.

I could still ask for a more obvious failure in my tests than I have here, so I think tomorrow I will try to implement a routing-only test. That could very well be the end of my testing exploration.


Day #213

No comments:

Post a Comment