Testing event binding and unbinding with Jasmine

Published by Lorenzo Planas on March 12, 2014

These days I'm experimenting with building JS apps with Riot.js, and forcing myself to implement most of the presenters' behavior using events, including interaction between presenters. This is leading me to write quite a few tests to check I'm properly binding to and unbinding from events.

I need to test for correctly bound events so my app works as intended. I also need to test for event unbinding, to reduce the chances of introducing memory leaks and zombie views.

Here's an example on how I'm doing it with vanilla Jasmine specs. Say we've got a mobile view showing a collection of items. We're going to focus on a single presenter (as the equivalent to Backbone views are called in Riot). I'm going to pick the presenter that will take care of handling an item in the collection.

Sample app

Testing event binding

Each item has a '.close' link, which will remove the item from the collection. In this example I'm handling the view-side of the event only. I won't be triggering any action in the model, which is just a dummy.

When we click on 'a.close', we will handle the event and remove the item itself. In this case, it makes sense the presenter should tear itself down.

Here's a code snippet for this behavior (check the full presenter and spec code here).

// Listen for DOM events and trigger applicable methods
this.domListen = function(element) {
  // Using jQuery's .on() for DOM events
  (element || this.$element).on("click", ".close", null, self.unload);
}

// Tear down this presenter
this.unload = function(e) {
  if (e) { e.preventDefault(); };
  self.unlisten.call(self);
  self.domUnlisten.call(self);
  self.remove.call(self);
};

// Remove from DOM
this.remove = function() {
  this.$element.remove();
};

Now, let's test that when we click on '.close', the 'unload' method will be called. There are several ways to do this. One is using Jasmine spies. Another approach, more barebones, is to override the handler method with a tracing function, which we can assert on in the test.

describe("#domListen", function() {
  it("binds to click on .close", function() {
    var timesTriggered = 0;
    presenter.unload = function() { timesTriggered = 1; };

    presenter.render();
    presenter.$close().trigger('click');
    expect(timesTriggered).toBe(1);
  });
});

In my apps I also use an event bus, mostly for view-to-view communication. Sometimes I use a single global bus, in other cases a global bus and some "regional" buses for subsets of views. That's moot in this case anyway, since we are going to inject the bus object. We will listen for an event signalling that the collection presenter (which "holds" the view for all individual items) is tearing itself down. Upon receiving the event, the item presenter will also tear itself down.

var ItemPresenter = function($element, options) {
  var self = this;

  this.$element = $element;

  // bus = $.observable({}) -- $.observable comes from Riot.js
  this.bus = options.bus;

  this.listen = function() {
    // This is Riot.js' .on() 
    this.bus.on("list:view:unload", self.unload);
  };

  // code omitted
}

To test it, we are going to use a similar approach as with the DOM events, overriding the handler method with a tracing function we can assert on in the spec.

describe("#listen", function() {
  it("binds to list:view:unload on the bus", function() {
    var timesTriggered = 0;
    presenter.unload = function() { timesTriggered += 1; }

    bus.trigger("list:view:unload");
    expect(timesTriggered).toBe(0);

    presenter.listen();
    bus.trigger("list:view:unload");
    expect(timesTriggered).toBe(1);
  });
});

Testing event unbinding

Now that we have the event handlers wired up, we need to ensure the presenter will unbind from all events when tearing itself down.

Let's start with DOM events:

// ******** Presenter code

this.domUnlisten = function(element) {
  // Using jQuery .off()
  (element || this.$element).off("click", ".close")
}

// ******** Spec code

describe("#domUnlisten", function() {
  it("unbinds from click on .close", function() {
    var timesTriggered = 0;
    presenter.unload = function() { timesTriggered += 1; };

    presenter.render();
    presenter.$close().trigger('click');
    expect(timesTriggered).toBe(1);

    timesTriggered = 0;
    presenter.domUnlisten();
    presenter.$close().trigger('click');
    expect(timesTriggered).toBe(0);
  });
});

And do the same with events on the bus:

// ******** Presenter code

this.unlisten = function() {
  // Using Riot.js .off()
  this.bus.off("list:view:unload", self.unload);
};

// ******** Spec code

describe("#unlisten", function() {
  it("unbinds from list:view:unload on the bus", function() {
    var timesTriggered = 0;
    presenter.unload = function() { timesTriggered += 1; };

    presenter.listen();
    bus.trigger("list:view:unload");
    expect(timesTriggered).toBe(1);

    timesTriggered = 0;
    presenter.unlisten();
    bus.trigger("list:view:unload");
    expect(timesTriggered).toBe(0);
  });
});

It wasn't that hard, was it? Still, it'll be a bit tiresome to do this for every single presenter in an app. This binding / unbinding logic can be extracted "a la Backbone", creating data structures in the presenters that associate events with handlers. I'm also going to try and implement a Jasmine linter, to check bus and DOM-related events are correctly bound / unbound when calling specific methods, according to a convention.

I'll be posting my progress, stay tuned :)

If you liked this post, you may find these interesting: