0

My code is functioning properly, but my test case is failing for one of its expectations. I don't understand why my spy fails to believe that a method has been executed.

I bind an event listener like so:

var playlists = Backbone.Collection.extend({
    initialize: function(){
        this.on('add', this._onAdd);
    },

    //  Method:
    _onAdd: function (model, collection, options) {
        console.log('onAdd');
        this._foo();
    },

    _foo: function(){
        console.log('foo');
    }
});

Playlists = new playlists();

and I am using sinon to spy on my object:

it('should call _onAdd when adding a model to the collection', function() {
    sinon.spy(Playlists, '_onAdd');
    sinon.spy(Playlists, '_foo');

    Playlists.add({});
    expect(Playlists._onAdd.calledOnce).to.equal(true);
    expect(Playlists._foo.calledOnce).to.equal(true);

    Playlists._onAdd.restore();
    Playlist._foo.restore();
});

My test case fails because the expectation for _onAdd being called once is not true. However, the expectation for _foo being called once is true.

I am doing something incorrect with how I am spying on my event listener. Why does sinon not believe _onAdd has been called. How can I correct this?

Sean Anderson
  • 27,963
  • 30
  • 126
  • 237

1 Answers1

7

Your problem is cause be function references. When you create the playlists in the Playlists variable it is running initialize() on the playlists collection which is setting up the event listener with a reference to this._onAdd.

When you make a spy it redefines what _onAdd points to, but doesn't update the reference the event listener is using. Consider the following:

_onAdd: function(){...} // Call this FuncRefA

When initialize is called (via new playlists();) the event listener will be evaluated to point at FuncRefA because this is what _onAdd points to at the time of evaluation.

this.on('add', FuncRefA);

Then when you add the spy, it is effectively doing something like this.

_onAdd: function(){ FuncRefA() } // Wraps the existing function, Call the new function FuncRefB

At this point _onAdd now points to FuncRefB, but the event listener is still pointing at FuncRefA so when your test runs the event listener is being call (and FuncRefA), but your spy is not being called, because the event listener doesn't know about it's existence (it is pointing a FuncRefA)

There are a couple of solutions:

1) You can setup the spy on the prototype on the playlists collection before you create Playlists, that way when Playlists is created is take the function reference to your spy

sinon.spy(playlists.prototype, '_onAdd');
Playlists = new playlists();
//... run tests now.

2) Make the event listener have an anonymous function that calls on _onAdd so that it is evaluated when the event happens. This will allow you to spy the Playlists's _onAdd method because it's reference is evaluated once the add event is triggered.

initialize: function(){
    var self = this;
    this.on('add', function(){ self._onAdd(arguments) });
},
Cubed Eye
  • 5,581
  • 4
  • 48
  • 64
  • 1
    This seems REALLY odd to me. How does one test a singleton without modifying code to support testing? Both of your solutions make sense and I understand why it is happening, but #2 is bad because it makes the code more convoluted to support spying and #1 is bad because it doesn't convey proper intent for my application. Playlists should only be allowed to be instantiated one time. – Sean Anderson Aug 30 '14 at 01:35
  • 1
    @SeanAnderson: Why is **1** problematic? I'd agree that **2** is a bit odd but why is instrumenting the prototype before instantiation bad? I'd actually say that checking if `_onAdd` is called a strange thing to be bothering with, why not test the behavior rather than the explicit call sequence? – mu is too short Aug 30 '14 at 01:45
  • Well, I can make **1** work with some refactoring, but I lose the ability to enforce Playlists as a singleton through design. Currently, Playlists instantiates itself inside of its scope and then exposes the singleton to the outside world. After refactoring, a parent object will need to instantiate Playlists and expose a single instance of it. I am checking _onAdd because I want to ensure that a method inside _onAdd was not executed based on parameters given to _onAdd, but that _onAdd itself was called. If _onAdd wasn't called then the test case could pass erroneously. – Sean Anderson Aug 30 '14 at 01:49