Sinon.js is a great library for unit testing JavaScript code. It has no dependencies and works with any unit test framework. In the future I plan to write up a more detailed overview of Sinon and its awesomeness. But today I want to quick share a little gotcha I recently ran into while unit testing a Backbone.js view using.
The Issue
As I have already stated, I was trying to unit test a function on a Backbone view. I wanted to attach a function spy on a method in my view and assert that it was called when a certain DOM element was clicked. Seems pretty simple right? Well, not exactly. Take a look at my backbone view and test code below.
define([
'text!./Templates/mainTemplate.html',
'text!./Templates/messageTemplate.html'
], function (
maintemplate,
messageTemplate
) {
'use strict';
return Backbone.View.extend({
// setup events
events: {
'click #detailsButton': 'displayMessage'
},
// compile templates
mainTemplate: _.template(mainTemplate),
messageTemplate: _.template(messageTemplate),
//..... functions removed for brevity
displayMessage: function (messageType, messageText) {
var that = this;
var $message = $(that.messageTemplate({
messageType: messageType,
messageText: messageText
}));
$('.closeButton', $message).click(function () {
$(this).closest('.infoBox').addClass('hide');
});
that.$('.infoBox').remove();
that.$('.content').append($message);
}
});
});
Above is my pretty simple backbone view, which is edited for brevity.
define(["testWidget.js"], function (Widget) {
"use strict";
var sandbox, testWidgetView;
module("TestWidget", {
setup: function () {
// create sinon sandbox
sandbox = sinon.sandbox.create();
// draw widget
testWidgetView = new Widget().render;
},
teardown: function () {
// cleanup sandbox
sandbox.restore();
}
});
test("widget displays message on click", function () {
// Arrange
var displayMessageSpy = sandbox.spy(testWidgetView, "displayMessage");
// Act
testWidgetView.$('#detailsButton').trigger("click");
// Assert -- FAILS
equal(displayMessageSpy.calledOnce, true, "displayMessage() was not called after click");
});
Above is my failing unit test, which had myself and a few other developers stumped me for a decent amount of time. While debugging through the code, I was able to see that the widget was rendering correctly, the function spy was being registered, and the displayMessage
function was being executed after the click event. So why in the world did the spy indicate that it was never called within the assert statement? Can you spot the issue before peaking at the solution?
The Solution
The problem arises because of the fact that the spy is created after view is rendered and the binding of events has already taken place. The key here is to understand that under the covers, Sinon actually wraps the original function with utilities to track invocations. Since your events have already been bound to original, unwrapped function, your wrapped function never actually gets called. To make sure that the events are bound to the spied version of the function, simply create the spy on the views prototype
before the view instantiates. In my case, the spy should be setup like this: sandbox.spy(testWidgetView.prototype, "displayMessage");
Here is the updated, and now passing, test.
define(["testWidget.js"], function(Widget) {
"use strict";
var sandbox, testWidgetView;
module("TestWidget", {
setup: function() {
// create sinon sandbox
sandbox = sinon.sandbox.create();
// do not instanatiate view in setup
},
teardown: function() {
// cleanup sandbox
sandbox.restore();
}
});
test("widget displays message on click", function() {
// Arrange
// attach the spy to the prototype BEFORE instantiating the view
var displayMessageSpy = sandbox.spy(testWidgetView.prototype, "displayMessage");
// instaniate view
testWidgetView = new Widget().render;
// Act
testWidgetView.$('#detailsButton').trigger("click");
// Assert - PASSES
equal(displayMessageSpy.calledOnce, true, "displayMessage() was not called after click");
});