10

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 = "firstname.name@email.com";
      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 = "firstname.name@email.com";
      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 : 'firstname.name@email.com', \
    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();
});
JJD
  • 44,755
  • 49
  • 183
  • 309
  • What happens if you remove the signInForm test? Or move it after the signUpForm test? – Tommi Komulainen May 24 '13 at 19:14
  • Perhaps something else is stopping the event and then retriggering after some action. Seems somewhat likely in a form submission that should not refresh the page. If the stopper doesn't trigger synchronously, then this spy will fail. – fncomp May 25 '13 at 02:12

2 Answers2

2

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?

Nick Sharp
  • 1,841
  • 10
  • 16
1

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?

Marc Greenstock
  • 10,636
  • 4
  • 27
  • 49