The Onist

The Onist


Tags


Function Spies in Backbone Views

Learn how to easily spy on and then test a function on a Backbone view using SinonJs.

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");
});
Share Post
View Comments