Stubbing an angularJs service with a function that returns a promise using sinon

1.2k views Asked by At

I'm trying to test a controller that calls a method on a service. The service method returns a promise, and the controller immediately invokes .then() inline after calling the service method. I'm trying to stub the service using sinon and Jasmine keeps throwing an error saying that then is undefined and not a function.

Here is the controller:

var loginModalController = function ($scope, authenticationService) {

    this.submit = submit;

    function submit(user, password) {
        $scope.email = user;
        authenticationService.login(user, password)
                .then(handleSuccessLogin, handleErrorLogin);
    }
}

Here is the service:

function authenticationService($http, $q, endPointService) {

    var baseUri = endPointService.getApiEndpoint();

    var service = {
        getTermsAndConditions: getTermsAndConditions,
        login: login,
        acceptTerms: acceptTerms
    };

    return service;

    function getTermsAndConditions() {
        ...
    };

    function login(user, password) {
        var deferred = $q.defer();
        $http({ method: 'POST', url: baseUri + '/api/tokens', data: { username: user, password: password } }).
           success(function (data, status, headers, config) {
               $http.defaults.headers.common.Authorization = 'Basic ' + data.EncryptedTokenId;
               deferred.resolve(data);
           }).
           error(function (data, status, headers, config) {
               deferred.reject(status);
           });
        return deferred.promise;
    };

    function acceptTerms() {
        ...
    };
}

And here is the test:

describe('loginModalController', function () {
    var scope, loginModalController, authenticationServiceMock, localSaverServiceMock;
    var loginInformationMock = { 'firstName': 'Testuser' };

    beforeEach(function () {
        module('clientAppModule');

        inject(function ($rootScope, $controller, authenticationService, localSaverService) {
            scope = $rootScope.$new();

            authenticationServiceMock = sinon.stub(authenticationService)
                .login.returns({ then: function () { return loginInformationMock } });

            localSaverServiceMock = sinon.stub(localSaverService);

            loginModalController = $controller('loginModalController', {
                $scope: scope,
                $state: {},
                authenticationService: authenticationServiceMock,
                errorCodes: {}, 
                localSaverService: localSaverServiceMock
            });
        });
    });

    it('should login', function () {
        loginModalController.submit("test", "test");
    });
});
1

There are 1 answers

0
TomSlick On BEST ANSWER

Four issues with my code:

  • I was unnecessarily using Sinon
  • I was using the return value of stub() rather than just letting it stub the service.
  • I wasn't using $q to return a deferred promise to match the login function.
  • I needed to call $digest() on the scope to get the deferred promise to resolve before asserting.

So here is the fixed test code:

beforeEach(module('clientAppModule'));

describe('loginModalController', function () {
    var scope, authenticationService, localSaverService;
    var loginInformationMock = { 'firstName': 'Testuser' };

    beforeEach(inject(function ($injector, $rootScope, $controller, $q) {

        scope = $rootScope.$new();
        scope.$close = function () { };

        authenticationService = $injector.get('authenticationService');
        localSaverService = $injector.get('localSaverService');            

        spyOn(authenticationService, 'login').and.callFake(function () {
            var deferred = $q.defer();
            deferred.resolve(loginInformationMock);

            return deferred.promise;
        });
        spyOn(localSaverService, 'saveLoginInformation').and.stub();

        $controller('loginModalController', {
            $scope: scope,
            $rootScope: {},
            $state: {},
            authenticationService: authenticationService,
            errorCodes: {},
            localSaverService: localSaverService
        });
    }));

    it('should call login on authenticationService', function () {
        // Arrange

        // Act
        scope.submit("test", "test");

        // Assert
        expect(authenticationService.login).toHaveBeenCalled();
    });

    it('should save login info after successful login', function () {
        // Arrange

        // Act
        scope.submit("test", "test");
        scope.$digest();

        // Assert
        expect(localSaverService.saveLoginInformation).toHaveBeenCalled();
    });
});