20

I have a backbone view and I want to create a test to confirm that a click event on some element will call the function bound to that element. My view is:

PromptView = Backbone.View.extend({
        id:"promptPage",
        attributes:{
            "data-role":"page",
            "data-theme":"a"
        },
        events:{
            "click #btnYes":    "answerYes",
            "tap #btnYes":      "answerYes"
        },
        render: function(){
            $(this.el).html(_.template($('#promptPage-template').html(), this.model.toJSON()));

            return this;
        },
        answerYes: function(){
            alert('yes');
        }
    });

My spec is:

beforeEach(function() {
            model = new PromptModel;
            view = new PromptView({model:model});
            loadFixtures('promptPage.tmpl');
        });

 it("should be able to answer a question with yes", function() {
                var button = $("#btnYes", view.render().el);
                expect(button.length).toBe(1);

                spyOn(view, 'answerYes');

                button.click();
                expect(view.answerYes).toHaveBeenCalled();

            });

However the above view definition creates the answerYes method on the prototype proto , but the spy creates a function on the actual instance in the view, so I end up with a view.answerYes() which is the spy and view.__proto__.answerYes, which is the one I actually want to spy on.

How can I create a spy so that it overrides the answerYes method of the view definition?

mishod
  • 1,040
  • 1
  • 10
  • 21
  • why do you want to spy on the prototype method? you have a view instance, and you are testing against that instance. you should be spying on the instance. – Derick Bailey Oct 26 '11 at 12:38
  • The only reason I thought this might be needed is because the test never passed when I used view.answerYes – mishod Oct 26 '11 at 15:27

5 Answers5

56

Hi I had today the same problem. And I have just found the solution, after creating the spyed method (answerYes) you have to refresh the events of the view to call that new spyed method ;) :

[...]

    spyOn(view, 'answerYes');
    view.delegateEvents();

    button.click();
    expect(view.answerYes).toHaveBeenCalled();

[...]

More information about delegate events

Have fun!

user2428118
  • 7,935
  • 4
  • 45
  • 72
Fernando Gm
  • 1,191
  • 9
  • 13
  • this helped! but is there a better way to do it, or do I always have to call delegateEvents() when I am spying on a view's methods? (standard practice?) – Jonathan Lin Nov 15 '12 at 08:26
  • Thank you so much! After several hours of trial + error I found this post. And FWIW this is usable with `Sinon.stub()` – cloakedninjas May 07 '13 at 14:33
3

I generally like to assume that the framework code already does what it ought to and only test my use of it, so I find it acceptable to have a test verifying the events hash. If I find myself duplicating backbone functionality in order to test my thing (like delegating events), then maybe I'm a step closer to integration tests than I really need to be. I also make heavy use of the prototype in order to be super-isolation lady in my unit tests. Of course, it is still important to have an integration layer that does all of that exercising, but I find the feedback loop too long for the test driving phase.

crebma
  • 71
  • 2
3

This creates a spy on the answerYes method of the PromptView:

spyOn(PromtView.prototype, 'answerYes');
Felipe Oriani
  • 37,948
  • 19
  • 131
  • 194
Parag Gupta
  • 131
  • 1
  • 3
1

TL;DR: Spy on the instance method, not the prototype.

I think you need to set up your test differently. There are too many concerns and too many expectations in the it block, and you're also polluting the global namespace, which can cause problems with tests.

beforeEach(function() {
  loadFixtures('promptPage.tmpl');

  var model = new PromptModel();
  this.view = new PromptView({model:model});
  this.view.render();

  this.button = this.view.$("#btnYes");
});

it("should render the button", function(){
  expect(this.button.length).toBe(1);
});

it("should be able to answer a question with yes", function() {
  spyOn(this.view, 'answerYes');

  this.button.click();
  expect(this.view.answerYes).toHaveBeenCalled();
});

You don't strictly need the expectation on the button's length. If the button has no length (not found), you'll get other failures. But you might want it there to make it easier to figure out the the view didn't render correctly.

You should also be spying on the view instance, as you've been doing. The definition of PromptView does add a answerYes method to the prototype, yes, but the one that you want to spy on is the view instance, not the prototype.

If you spy on the prototype's method, then every time you try to use this view in your tests, the answerYes method will be the spy, not the actual method. This may sound nice, but it will cause problems as you won't have access to valid spy data when you call this method multiple times. It will simply accumulate all of the calls on that one spy. If you try to spy on the prototype method twice, you may end up with a spy of a spy, which would be a strange thing and could cause problems.

Derick Bailey
  • 72,004
  • 22
  • 206
  • 219
  • 1
    Derick the tip on changing the structure is a nice touch, but there is no substantial change in the test other that declaring the view on _this_. However when I run this test I still get an alert, instead of heaving the spy execute. The expectation also fails and I don't get it why. – mishod Oct 26 '11 at 15:48
  • Is it possible that the backbone binding somehow affects the outcome? – mishod Oct 26 '11 at 15:54
0

If you have trouble using spyOn, you could consider creating a spy. So something like:

var eventSpy;
eventSpy = jasmine.createSpy('eventSpy');
view.$el.on('myCustom:event', eventSpy);
David A
  • 717
  • 1
  • 8
  • 18