20

I am trying to test a button click using backbone.js, jasmine.js and sinon.js. But the following test case fails. I am using a spy to track whether it is getting called or not. Can you please help me with this?

Thanks.

New Task Template

<script id='new_task_template' type='text/template'>
  <input type='text' id='new_task_name' name='new_task_name'></input>
  <button type='button' id='add_new_task' name='add_new_task'>Add Task</button>
</script>

NewTaskView

T.views.NewTaskView = Backbone.View.extend({
  tagName: 'section',
  id: 'new_task_section',
  template : _.template ( $("#new_task_template").html() ),
  initialize: function(){
    _.bindAll( this, 'render', 'addTask');
  },
  events:{
    "click #add_new_task" : "addTask"
  },
  render: function(){
    $(this.el).html( this.template() );
    return this;
  },
  addTask: function(event){
    console.log("addTask");
  }
});

Jasmine Test Case

describe("NewTaskView", function(){
  beforeEach( function(){    
    this.view = new T.views.NewTaskView();
    this.view.render();
  });

  it("should #add_new_task is clicked, it should trigger the addTask method", function(){
    var clickSpy = sinon.spy( this.view, 'addTask');
    $("#add_new_task").click();
    expect( clickSpy ).toHaveBeenCalled();
  });
});

Jasmine Output

NewTaskView
  runEvents
    runshould #add_new_task is clicked, it should trigger the addTask method
      Expected Function to have been called.
felix
  • 11,304
  • 13
  • 69
  • 95
  • Where in the DOM does the view render itself? Is the template even available within the specs? Probably button#add_new_task doesn't exist in the DOM of the Jasmine spec runner and therefore `$("#add_new_task").click();` has no effect. If you're sure that the view is being rendered with the correct template, you could use NewTaskView's element as context for the jquery function: `$('#add_new_task', this.view.el).click();`. – Julian Maicher Feb 02 '12 at 15:24
  • I think this question is already answered here: http://stackoverflow.com/questions/8441612/why-is-this-sinon-spy-not-being-called-when-i-run-this-test/9012788#9012788 – vadimich Apr 30 '14 at 22:39

3 Answers3

41

The problem is you add your spy after backbone has already bound the click event directly to the addTask function (it does that during construction of the View). Therefore your spy will not get called.

Try attaching the spy to a prototype of the View before you construct it. Like this:

this.addTaskSpy = sinon.spy(T.views.NewViewTask.prototype, 'addTaskSpy');
this.view = new T.views.NewTaskView();

and then remember to remove it:

T.views.NewViewTask.prototype.addTaskSpy.restore()
brianng
  • 5,790
  • 1
  • 32
  • 23
John Roberts
  • 676
  • 6
  • 5
  • 4
    This answer should be accepted. Thank you, this was such a headache! – backus Aug 29 '12 at 22:51
  • Makes total sense! Thank you! :) – nuc Sep 12 '12 at 13:30
  • 2
    what it's not clear to me is: why do I need to spy the protype if I want to spy just an instance of the prototype. Is that impossible? – dierre Jul 15 '13 at 10:22
  • This worked great. Much better than having to change my code to work with my tests. – mwilcox Mar 26 '14 at 20:47
  • 1
    +1 Thanks! Just a small comment, because the method to spy on is addTask it should be: `this.addTaskSpy = sinon.spy(T.views.NewViewTask.prototype, 'addTask');` and `T.views.NewViewTask.prototype.addTask.restore()` – klode Oct 04 '14 at 16:53
0
events:{
    "click" : "addTask"
},

means that you are binding the click event to this.el - root element of the view which in your case have an ID new_task_section. You need to bind it to #add_new_task which is your 'add task' button I assume - this should fix it!

events:{
    "click #add_new_task" : "addTask"
},

Update:

$("#add_new_task") won't find the element as the view isn't added to the document DOM tree. use this.view.$('#add_new_task') and it should work as it will search for the element in the detached fragment stored in the view.

Tom Tu
  • 9,573
  • 35
  • 37
  • Actually I also had this "click #add_new_task" and the event gets fired as I have console.log line in the addTask method. but expect(spy).toHaveBeenCalled() fails; May be my binding is wrong? – felix Feb 03 '12 at 03:00
  • if #add_new_task is not in this view, isn't it being binded in the wrong place? – Tjorriemorrie Feb 03 '12 at 05:54
  • the way jQuery works it searches the DOM tree attached to the document, but if you don't attach the Backbone view element to the DOM tree yourself it's simply not there. That's why you need to use the Backbone jQuery find (Backbone.View $ method) which sets the context of the search to your View's `el` making it possible to search in a bit of DOM which isn't attached to the DOM tree yet. #add_new_task is precisely in the view - it's just the view isn't in the document - it's just kept in the memory and waits to be inserted – Tom Tu Feb 03 '12 at 08:58
  • try doing `$(body).append(this.view.el)` after render and you'll see that it should then work without changing the $('#add_new_task') to `this.view.$('#add_new_task')` (or you could write it `$('#add_new_task', this.view.el)` if you prefer). It's 100% correct behavior – Tom Tu Feb 03 '12 at 09:00
  • 1
    The `jasmine-jquery` helpers could help you with your fixtures: https://github.com/velesin/jasmine-jquery – Julian Maicher Feb 03 '12 at 09:56
  • @TomTu I changed the code to this.view.$("#add_new_task").I still get the same error but the event is getting just as earlier :( – felix Feb 03 '12 at 20:05
  • Hmm... If anybody won't manage to help before i'll try to dig deeper over the weekend :) – Tom Tu Feb 03 '12 at 20:24
  • @TomTu Thanks :) I will also continue to find the cause of the problem. – felix Feb 04 '12 at 03:42
0

There are some problems with your approach. First you spy on your class you wanna test, with is not the way a unit test should work cause you test the inner logic of your class and not its behavior. Second, and thats the reason why your test fail, you dont have attached your views el into the DOM. So either you attache your el to the DOM or fire the click event directly to the el: $('#add_new_task', this.view.el).click().

Btw. backbones way to create element and bind events makes it hard to write good unit tests cause you be forced to work with the DOM and jquery. A better way to write testable code would be to always pass all dependencies in to the constructor and dont create new instances in your code cause its hard to test these objects. So in your case it would be much easier to inject the el object in the constructor as a jquery object and at the events manually. Doing it this way you can test you class without any dependencies to the DOM or jquery.

So in your case the constructor would look like this:

initialize: function(){
   this.el.click(_.bind( this, 'addTask'));
}

And your test:

var el = {click: function(){}};
spyOn( el, 'click');
new T.views.NewTaskView({el: el});
expect(el.click).toHaveBeenCalled(); //test the click event was bind
//call the function that was bind to the click event, 
//which is the same as trigger the event on a real DOM object
el.click.mostRecentCall.args[0]() 

After all you have to decide what approach will fit your needs. Leaner code with backbones helpers or better testable code with less dependencies to jquery, DOM and backbone.

Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • Jasmine is not a Unit Testing framework - it's BDD framework and what he does is perfectly fine - testing a behavior – Tom Tu Feb 03 '12 at 10:59
  • Not sure whats your point. Spying on the class you wanna test made not much sense. Cause its not the behavior of your class to call a specific function on itself. Changing the name of this function will break the test but not the behavior. The other think is that I think its better to mock out as much dependencies as you can. Using dependency injection makes the much easier cause you dont have to load a fixture before your test, fire a click event on it and clean up afterwards. – Andreas Köberle Feb 03 '12 at 14:29