Tuesday, September 13, 2011

Outside-in Backbone.js Development with Jasmine

‹prev | My Chain | next›

Having thought about, some of my Jasmine testing woes may be due, in part, to testing too much. Ultimately, I would like to test that a couple of clicks can open an edit dialog and save changes in my Backbone.js application.

This is not working. Since I am exercising such a large swath of my app, I am unsure where the breakdown is occurring. So instead of testing that I can update the screen via an edit dialog, (which updates the model, which updates the screen):
  it("can edit appointments through an edit dialog", function() {
    $('.appointment', '#2011-09-15').click();

    $('.ok', '#dialog').click();
    server.respondWith('PUT', '/appointments/42', '{"title":"Changed!!!"}');
    server.respond();

    expect($('#2011-09-15')).toHaveText(/Changed/);
  });
Instead I will test that updating the model results in a change on the screen:
  it("displays model updates", function () {
    var appointment = Appointments.at(0);
    appointment.save({title: "Changed"});

    server.respondWith('PUT', '/appointments/42', '{"title":"Changed!!!"}');
    server.respond();

    expect($('#2011-09-15')).toHaveText(/Changed/);
  });
That fails:
It is still failing, but I have a much better idea of where things are going wrong. Either the server.responseWith() is not returning the results in a recognizable format or...

Oh man!

Or I am not updating the view in response to model changes. By trying to implement the edit-dialog feature in one fell swoop, I completely forgot to bind the View to model change events. All I need to do is re-render the appointment view when an appointment model emits a "change" event:
  window.AppointmentView = Backbone.View.extend({
    template: _.template($('#calendar-appointment-template').html()),
    initialize: function(options) {
      this.container = $('#' + this.model.get('startDate'));
      options.model.bind('destroy', this.remove, this);
      options.model.bind('error', this.deleteError, this);
      options.model.bind('change', this.render, this);
    },
    render: function() {
      $(this.el).html(this.template(this.model.toJSON()));
      this.container.append($(this.el));
      return this;
    },
    // ...
  });
With that, my new, smaller test passes:
Gah! I made myself crazy over that end-to-end test and I was just missing a tiny bit of track. I think I may have learned me a valuable lesson here today. It is possible to test high-level and low-level in Jasmine. Make damn sure you know which you ought to be doing.

At any rate, I can now go back to my original, end-to-end test:
  it("can edit appointments through an edit dialog", function() {
    $('.appointment', '#2011-09-15').click();
    $('.ok').click();

    server.respondWith('PUT', '/appointments/42', '{"title":"Changed!!!"}');
    server.respond();

    expect($('#2011-09-15')).toHaveText(/Changed/);
  });
I can make that test pass my misappropriating the "create" method on my "Application View":
  window.AppView = Backbone.View.extend({
    el: $("#dialog").parent(),
    events: {
      'click .ok':  'create'
    },
    create: function() {
      // Edit -- the first appointment...
      var appointment = Appointments.at(0);
      appointment.save({title: $('input.title', '#dialog').val()});
      return;

      // Actual create code used to be here...
    }
  });
With that, I have all of my tests passing:
That is a pretty useless edit dialog (and it breaks the create dialog), but it does make my test pass. I will claim that as a moral victory.

I think that is a good stopping point for tonight. I revert my silly edit-inside-the-create dialog code. This leaves me with a failing test as a reminder where to pick up tomorrow. All in all, I am in much better shape than when I started. i have a new, focused, passing test describing screen updates after a model change. Even better, I know that I can get end-to-end testing working -- I just need a specialized edit dialog. Tomorrow.


Day #132

No comments:

Post a Comment