In the following unit test code:
TestModel = Backbone.Model.extend({
defaults: {
'selection': null
},
initialize: function() {
this.on('change:selection', this.doSomething);
},
doSomething: function() {
console.log("Something has been done.");
}
});
module("Test", {
setup: function() {
this.testModel = new TestModel();
}
});
test("intra-model event bindings", function() {
this.spy(this.testModel, 'doSomething');
ok(!this.testModel.doSomething.called);
this.testModel.doSomething();
ok(this.testModel.doSomething.calledOnce);
this.testModel.set('selection','something new');
ok(this.testModel.doSomething.calledTwice); //this test should past, but fails. Console shows two "Something has been done" logs.
});
The third ok fails, even though the function was effectively called from the backbone event binding, as demo'd by the console.
This is very frustrating and has shaken my confidence on whether sinon.js is suitable for testing my backbone app. Am I doing something wrong, or is this a problem with how sinon detects whether something has been called? Is there a workaround?
EDIT: Here's a solution to my specific example, based on the monkey patch method of the accepted answer. While its a few lines of extra setup code in the test itself, (I don't need the module function any more) it gets the job done. Thanks, mu is too short
test("intra-model event bindings", function() {
var that = this;
var init = TestModel.prototype.initialize;
TestModel.prototype.initialize = function() {
that.spy(this, 'doSomething');
init.call(this);
};
this.testModel = new TestModel();
. . . // tests pass!
});
Calling
this.spy(this.testModel, 'doSomething')
replaces thetestModel.doSomething
method with a new wrapper method:So
this.spy(this.testModel, 'doSomething')
is effectively doing something like this:This means that
testModel.doSomething
is a different function when you bind the event handler ininitialize
:than it is after you've attached your spying. The Backbone event dispatcher will call the original
doSomething
method but that one doesn't have the Sinon instrumentation. When you calldoSomething
manually, you're calling the new function thatspy
added and that one does have the Sinon instrumentation.If you want to use Sinon to test your Backbone events, then you'll have to arrange to have the Sinon
spy
call applied to the model before you bind any event handlers and that probably means hooking intoinitialize
.Maybe you could monkey-patch your model's
initialize
to add the necessaryspy
calls before it binds any event handlers:Demo: http://jsfiddle.net/ambiguous/C4fnX/1/
You could also try subclassing your model with something like:
And then use
TestModel
instead of Model, this would give you an instrumented version ofModel
inTestModel
without having to include a bunch of test-specific code inside your normal production-readyModel
. The downside is that anything else that usesModel
would need to be subclassed/patched/... to useTestModel
instead.Demo: http://jsfiddle.net/ambiguous/yH3FE/1/
You might be able to get around the
TestModel
problem with:but you'd have to get the ordering right to make sure that everyone used the new
Model
rather than the old one.Demo: http://jsfiddle.net/ambiguous/u3vgF/1/