Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jasmine test using .toHaveBeenCalledWith fails for sign-up form

The single page application I am working on has a login view with two forms: a sign-in form and a sign-up form. The following spec describes tests for these forms. I am using Jasmine-jQuery 1.4.2.

// user_spec.js

describe("User", function() {

  var userController;

  beforeEach(function () {
    loadFixtures('menu.html');
    userController = new MyApp.User.Controller();
  });

  describe("LoginView", function() {

    beforeEach(function() {
      // Mock the $.ajax function to prevent XHR:
      spyOn($, "ajax").andCallFake(function(params) {});
    });

    it("should pass email and password with the 'signInForm:submit' event.", function() {
      var email = "[email protected]";
      var password = "Passw0rd";
      var callback = jasmine.createSpy("FormSubmitSpy");

      userController.loginView.$el.find("#signInEmail").val(email);
      userController.loginView.$el.find("#signInPassword").val(password);
      userController.loginView.bind("signInForm:submit", callback, this);
      userController.loginView.ui.signInForm.trigger("submit");

      expect(callback).toHaveBeenCalledWith({
        email: email,
        password: password
      });
    });

    it("should pass name, email and password with the 'signUpForm:submit' event.", function() {
      var name = "John Doe";
      var email = "[email protected]";
      var password = "Passw0rd";
      var callback = jasmine.createSpy("FormSubmitSpy");

      userController.loginView.$el.find("#signUpName").val(name);
      userController.loginView.$el.find("#signUpMail").val(email);
      userController.loginView.$el.find("#signUpPassword").val(password);
      userController.loginView.$el.find("#signUpPasswordConfirmation").val(password);
      userController.loginView.bind("signUpForm:submit", callback, this);

      userController.loginView.ui.signUpForm.trigger("submit");

      expect(callback).toHaveBeenCalledWith({
        name: name,
        email: email,
        password: password,
        password_confirmation: password
      });
    });

  });

});

The test for the sign-in form runs successful however the test for the sign-up form fails.

Error: Expected spy FormSubmitSpy to have been called with \
    [ { name : 'John Doe', email : '[email protected]', \
    password : 'Passw0rd', password_confirmation : 'Passw0rd' } ] \
    but it was never called.

    at new jasmine.ExpectationResult (http://localhost:3000/assets/jasmine.js?body=1:114:32)
    at null.toHaveBeenCalledWith (http://localhost:3000/assets/jasmine.js?body=1:1235:29)
    at null.<anonymous> (http://localhost:3000/assets/user_spec.js?body=1:233:24)
    at jasmine.Block.execute (http://localhost:3000/assets/jasmine.js?body=1:1064:17)
    at jasmine.Queue.next_ (http://localhost:3000/assets/jasmine.js?body=1:2096:31)
    at jasmine.Queue.start (http://localhost:3000/assets/jasmine.js?body=1:2049:8)
    at jasmine.Spec.execute (http://localhost:3000/assets/jasmine.js?body=1:2376:14)
    at jasmine.Queue.next_ (http://localhost:3000/assets/jasmine.js?body=1:2096:31)
    at jasmine.Queue.start (http://localhost:3000/assets/jasmine.js?body=1:2049:8)
    at jasmine.Suite.execute (http://localhost:3000/assets/jasmine.js?body=1:2521:14)

Using the forms in the application there is no problem. Data is transmitted. Everything works fine. Just the test does not.

Workaround

The test however is successful when I delay its execution.

_.defer(function() {
  expect(callback).toHaveBeenCalledWith({
    name: name,
    email: email,
    password: password,
    password_confirmation: password
  });
});

Why does this work and the "normal" implementation fails?


Here is a simplification of the given case:

it("should evaluate true", function() {
  var foo = false;
  _.defer(function() {
    foo = true;
  });
  expect(foo).toBeTruthy();
});
like image 521
JJD Avatar asked Apr 21 '13 16:04

JJD


2 Answers

The Jasmine way to do the same thing without using an underscore function for deferral would be the following:

var flag = false;
...

runs(function() {
  userController.loginView.ui.signInForm.trigger("submit");
  setTimeout(function() { flag = true; }, 1);
}

waitsFor(function() {
  return flag;
}, "Blah this should never happen", 10);

runs(function() {
  expect(callback).toHaveBeenCalledWith({
    name: name,
    email: email,
    password: password,
    password_confirmation: password
  });
}

@Marc is correct that the issue is with using bind and the way Javascript sends events "sometimes/usually/always" into he next event loop (its how its asynchronous nature works), so since you are spying on a callback you want to make sure that your tests are written to account for that asynchronous behavior.

As your tests are written you run the risk that the first test won't pass either sporadically (I'm surprised it works as is). You are testing an asynchronous event callback in a non asynchronous fashion... make sense?

like image 192
Nick Sharp Avatar answered Jan 04 '23 07:01

Nick Sharp


The reason you are seeing this problem is because the callback is nested in some other method, probably jQuery bind and jasmine's spy wraps directly over your method so when the test is set up your method isn't executed until the next tick.

I found this page: http://trevmex.com/post/7017702464/nesting-spies-to-see-if-a-callback-in-a-deep-nest-has useful as it describes your problem and a potential work around.

However I've discovered it's best not to necessarily test callbacks as they can be considered private methods. Perhaps you can test its end result?

like image 30
Marc Greenstock Avatar answered Jan 04 '23 07:01

Marc Greenstock