Search code examples
backbone.jskarma-runnersinonbackbone-eventssinon-chai

How am I able to test that a Backbone model's method bound to the event bus has fired when using Karma and Sinon?


When testing that a backbone model's event has fired with a sinon spy, it erroneously errors: expected doSomething to be called once but was called 0 times, even though it seems to execute when a console log is put in the method's body. The testing function looks like:

it('Y U NO WORK', function() {
    const events = {};
    _.extend(events, Backbone.Events);
    const Model = Backbone.Model.extend({
        initialize: function() {
            this.listenTo(events, 'doSomething', this.doSomething);
        },
        doSomething: function() {},
    });
    const model = new Model();
    const spy = sinon.spy(model, 'doSomething');
    events.trigger('doSomething');
    sinon.assert.calledOnce(spy);
});

I know that to fix, you'd have to put the sinon spy on the Model's prototype like const spy = sinon.spy(Model.prototype, 'doSomething'); in the line before the new Model() call, however it seems to work without issue when put in the model instance, like below:

it('And this does work', function() {
    const Model = Backbone.Model.extend();
    const model = new Model();
    const spy = sinon.spy(model, 'set');
    model.set('foo', 'bar');
    sinon.assert.calledOnce(spy);
});

Curious why it needs to be put on the model's prototype in the first instance, but works on the model instance in the second?


Solution

  • Spy kind of replaces the original method with a custom one to know when it's invoked (It's holds reference to original for restoring later). So in the first case you set up an event listener before creating the spy. The event system is actually holding direct reference to the original method, not to the spy. Spy can do nothing about it, Spy wouldn't know when it is invoked.

    You need to set up the spy first before setting up the event listener, something like:

    it('Y U NO WORK', function() {
        var spy;
        const events = {};
        _.extend(events, Backbone.Events);
        const Model = Backbone.Model.extend({
            initialize: function() {
                spy = sinon.spy(this, 'doSomething');
                this.listenTo(events, 'doSomething', this.doSomething);
                //now same as this.listenTo(events, 'doSomething', spy);
            },
            doSomething: function() {},
        });
        const model = new Model();
        events.trigger('doSomething');
        sinon.assert.calledOnce(spy);
    });
    

    Or avoid keeping direct references to the original method like:

    this.listenTo(events, 'doSomething', function() {
      //by the time this is invoked, original has been replaced with spy
      this.doSomething();
    });
    

    It'd work because it's not holding reference to original method, the method invokation is dynamic