2

I'm using backbone.validation in order to validate my backbone Models and am employing a TDD approach to my work. Unfortunately I cannot seem to get my spy to be called when testing that fields are actually required.

I've been following the tutorial on Testing Backbone applications with Jasmine and Sinon except when he registers his spy with "error"I've tried registering mine with "invalid". The reason for this is because I think backbone.validation uses invalid/valid callbacks instead as described under the Events section of the readme.

My problem is that I'm getting errors saying that my spies are never called. I tried changing the bindings back to error/save but still have no luck.

My code is as follows:

class Event extends Backbone.Model
    url: ->
      '/events' + (if @isNew() then '' else '/' + @id)

    validation:
      title:
        required: true
      start_date:
        required: true
      end_date:
        required: true
      description:
        required: true

I then define a test as follows:

describe "Event", ->
  beforeEach ->
    @title = "Foo"
    @description = "Bar"
    @start_date = new Date
    @end_date = new Date


    @event = new CPP.Models.Event {
      title: @title
      description: @description
      start_date: @start_date
      end_date: @end_date
    }

  describe "when saving required fields", ->
    beforeEach ->
      @error_spy = sinon.spy();
      @event.bind('invalid', @error_spy)

    it "should not save when title is empty", ->
      @event.save 'title': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when start_date is empty", ->
      @event.save 'start_date': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when end_date is empty", ->
      @event.save 'end_date': ""l
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when description is empty", ->
      @event.save 'description': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when location is empty", ->
      @event.save 'location': null
      expect(@error_spy).toHaveBeenCalledOnce();

  describe "when saving optional fields", ->
    beforeEach ->
      @success_spy = sinon.spy();
      @event.bind('valid', @success_spy)

    it "should save when deadline is empty", ->
      @event.save 'deadline': ""
      expect(@success_spy).toHaveBeenCalledOnce();

However when I run my tests I seem to get Error: Expected Function to have been called once. for all of them and on further inspection of the @event object I find that the spy is never called.

I think it might be something to do with mixing in the validation on the Model's prototype via _.extend(Backbone.Model.prototype, Backbone.Validation.mixin); as defined on the backbone.validation readme, but I cannot seem to get this to work either.

I've had a look at the question Why is this sinon spy not being called when I run this test? however I've had no luck with incorporating the answer into my code either.

If anyone could tell me what I'm doing wrong I'd be very grateful!

Fixed I managed to fix my code as follows: (1) I added _.extend Backbone.Model::, Backbone.Validation.mixin into my application

(2) I then followed the advice given in this question to bind my spy on initialize. The code now looks as follows: The model: class Event extends Backbone.Model url: -> '/events' + (if @isNew() then '' else '/' + @id)

    validation:
      title:
        required: true
      start_date:
        required: true
      end_date:
        required: true
      description:
        required: true

The test:

describe "Event", ->
  beforeEach ->
    @title = "Foo"
    @description = "Bar"
    @start_date = new Date
    @end_date = new Date

  describe "when saving required fields", ->
    beforeEach ->
      spy = @error_spy = sinon.spy();
      init = CPP.Models.Event::initialize
      CPP.Models.Event::initialize = ->
        spy(@, "validated:invalid")
        init.call this

      @event = new CPP.Models.Event {
        title: @title
        description: @description
        location: @location
        start_date: @start_date
        end_date: @end_date
      }

    it "should not save when title is empty", ->
      @event.save 'title': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when start_date is empty", ->
      @event.save 'start_date': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when end_date is empty", ->
      @event.save 'end_date': ""l
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when description is empty", ->
      @event.save 'description': ""
      expect(@error_spy).toHaveBeenCalledOnce();

    it "should not save when location is empty", ->
      @event.save 'location': null
      expect(@error_spy).toHaveBeenCalledOnce();

  describe "when saving optional fields", ->

    beforeEach ->
      spy = @success_spy = sinon.spy();
      init = CPP.Models.Event::initialize
      CPP.Models.Event::initialize = ->
        spy(@, "validated:valid")
        init.call this

      @event = new CPP.Models.Event {
        title: @title
        description: @description
        location: @location
        start_date: @start_date
        end_date: @end_date
      }

    it "should save when deadline is empty", ->
      @event.save 'deadline': ""
      expect(@success_spy).toHaveBeenCalledOnce();
Community
  • 1
  • 1
Sarah Tattersall
  • 1,275
  • 2
  • 21
  • 32
  • What if you change `@event.bind('invalid', @error_spy)` to `Event.prototype.bind('invalid', @error_spy)`? – Chris Salzberg Nov 03 '12 at 09:21
  • I then get the error `Object # has no method 'bind'` – Sarah Tattersall Nov 03 '12 at 09:34
  • Sorry I'll have a look at this in detail later if nobody else has answered. – Chris Salzberg Nov 03 '12 at 09:46
  • Shouldn't you be binding `validated:invalid` and not `invalid`? I've never used backbone.validation but just reading the docs that seems to be the event name. – Chris Salzberg Nov 03 '12 at 22:31
  • p.s. I don't think the problem is related to the mixing in of `Backbone.Validation.mixin` to the model prototype. The other SO answer is relevant if you are spying on a method that is triggered by an (already bound) event, but in this case you are binding a spy to an event yourself, so this shouldn't be a problem. – Chris Salzberg Nov 03 '12 at 22:34
  • Just to be sure, can you try: expect(spy.callCount).to.equal(1) – julesbou Nov 03 '12 at 23:19

2 Answers2

2

Have you checked that you are including the code to add the validation mixins?

If you want to be able to bind to the validated events in your models throughout your application rather than to a specific view then you need to add the mixins by writing

_.extend(Backbone.Model.prototype, Backbone.Validation.mixin)

which can be written in your application using coffeescript as

_.extend Backbone.Model::, Backbone.Validation.mixin

Add this code in your main Backbone app file.

Once you've done this, your spy issues may be linked to this question - You need to check you're binding spy at the right time, before you bind any event handlers. The solution in the previous link does this by hooking into initialize.

Community
  • 1
  • 1
Pete Hamilton
  • 7,730
  • 6
  • 33
  • 58
0

I think you're just binding to the wrong event. From the fine manual:

validated

The validated event is triggered after validation is performed, either it was successful or not. isValid is true or false depending on the result of the validation.

model.bind('validated', function(isValid, model, errors) {
  // do something
});

validated:valid

The validated:valid event is triggered after a successful validation is performed.

model.bind('validated:valid', function(model) {
  // do something
});

validated:invalid

The validated:invalid event is triggered after an unsuccessful validation is performed.

model.bind('validated:invalid', function(model, errors) {
  // do something
});

So there isn't an 'invalid' event, there is a 'validated:invalid' event though. Try this:

@event.bind('validated:invalid', @error_spy)
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • I tried this first and still get the same message `Expected Function to have been called once.` I'll post the result in an update – Sarah Tattersall Nov 04 '12 at 18:43
  • @Sarah: You could try binding a `-> console.log(arguments)` to the `"all"` event to see what (if anything) does get triggered. – mu is too short Nov 04 '12 at 18:48
  • It turns out "error" is being called and when I bind -> console.log "ERROR" to "error" it displays "ERROR" but when I bind the spy to it it doesn't think it's been called. – Sarah Tattersall Nov 04 '12 at 18:55
  • You could try adding a function validator (see the `someAttribute` example in the [documentation](https://github.com/thedersen/backbone.validation/blob/master/README.md)) just to see if the validators are getting called. – mu is too short Nov 04 '12 at 19:01
  • I added it in but I'm not sure where I'd see the error, I expect in a callback that isn't happening somewhere? Strangely I can't call model.isValid() because apparently the event has no method "validate". Could this be a clue? – Sarah Tattersall Nov 04 '12 at 19:15
  • Sounds like a clue to me. The plugin should be adding `isValid` and `validate` methods to Backbone.Model so maybe the mixin isn't being mixed in for your tests. If you throw a `console.log('Validator called!')` into a validation callback then you'd at least know that the validators are getting called. – mu is too short Nov 04 '12 at 19:22