how to return rejected/failure promise to through jasmine spy to an angular controller in a unit test

Nishutosh Sharma picture Nishutosh Sharma · Jul 28, 2014 · Viewed 15.4k times · Source

I am using jasmine for testing my angular controllers. I am catching errors and success in the .then(successCallback, errorCallback) Although it is working fine on the bases of live functionality but am confused how to write a spy for returning an error as it is always caught in the successCallback()

Following is the controller :-

angular.module('myApp')
.controller('LoginCtrl', function ($scope, $location, loginService, SessionService) {
    $scope.errorMessage = '';

    $scope.login = function () {

      var credentials = {
          email: this.email,
          password: this.password
      };

     SessionService.resetSession();
     var request = loginService.login(credentials);

     request.then(function(promise){ //successfull callback
        if (promise['status'] === 200){
             //console.log('login');
            $location.path('/afterloginpath');
        }
      },
     function(errors){  //fail call back
      //  console.log(errors);
        $location.path('/login');
     });
    };
});

My test case :-

'use strict';

describe('Controller: LoginCtrl', function () {

  // load the controller's module
  beforeEach(module('myApp'));

  var LoginCtrl, scope, location, login, loginReturn, session;
  var credentials = {'email': '[email protected]', 'password': 'admin123'};

  // Initialize the controller and a mock scope
 beforeEach(inject(function ($controller, $rootScope, $location, _loginService_, _SessionService_) {

    scope = $rootScope.$new();
    LoginCtrl = $controller('LoginCtrl', {
      $scope: scope
    });

    location = $location;
    login = _loginService_;
    session = _SessionService_;
    scope.errorMessage = '';
    spyOn(login, "login").andCallFake(
      function(){
        return {
          then: function(response){
            response(loginReturn);
          }
        }
      }
    );

    spyOn(session, "setName").andCallFake(function(){
      return true;
    });
  }));

  it('should go to login when login fail', function () {
    loginReturn = function(){
      return{
        successfullyCallback: {throwError:true},
        failCallback: {status: 400, 'data' : {'errors' : [{"type":"invalid_data","target":"email,password"}]}}
      }
    };

    var wrong_creds = {email: '[email protected]', password: 'wrong_password'};
    scope.email = wrong_creds.email;
    scope.password = wrong_creds.password;

scope.login();

    expect(location.path()).toBe("/login");
    expect(scope.errorMessage).toBe('username or password combination is invalid');
  });

});

Answer

ivarni picture ivarni · Jul 28, 2014

I find it easier to use actual promises for mocked services as it removes a lot of nested functions and is a lot easier to read.

Relevant snippets ($q needs to be injected in beforeEach):

deferred = $q.defer();
spyOn(login, 'login').andReturn(deferred.promise);

...

deferred.reject({ ... });

After resolving or rejecting the promise you need to call scope.$digest() so angular can process it.