Jasmine controller testing, expected spy to have been called

31,133

Solution 1

You are spying on the mock after it has been passed into the instantiated controller.

Try this:

describe('usersAddUserControllerUnitTest', function () {
    var scope, deferred, objectUnderTest, mockedAddUserService, $controller;

    beforeEach(module('app'));

    beforeEach(inject(function ($rootScope, $q, _$controller_) {
        scope = $rootScope.$new();

        function emptyPromise() {
            deferred = $q.defer();
            return deferred.promise;
        }

        mockedAddUserService = {
            getCountryPhoneCodes: emptyPromise
        };

        $controller = _$controller_;
    }));

    function makeController() {    
        objectUnderTest = $controller('usersAddUserController', {
            $scope: scope,
            usersAddUserService: mockedAddUserService
        });
    }

    it('should call getCountryPhoneCodes method on init', function () {
        //when

        spyOn(mockedAddUserService, 'getCountryPhoneCodes').and.callThrough();
        makeController();

        deferred.resolve();

        scope.$root.$digest();

        //then
        expect(mockedAddUserService.getCountryPhoneCodes).toHaveBeenCalled();
    });

});

EDIT Thanks @juunas for noticing the bug in my solution

Solution 2

You can provide the mock like this:

mockedAddUserService = {
    getCountryPhoneCodes: emptyPromise
};


beforeEach(function () {
    module(function ($provide) {
        $provide.value('usersAddUserService', mockedAddUserService);
    });
});

EDIT:

The code should look (as i cannot test it) like this:

(function () {
    'use strict';

    describe('usersAddUserControllerUnitTest', function () {        
        beforeEach(module('app'));

        var emptyPromise = function() {
            var deferred = $q.defer();
            return deferred.promise;
        }

        var mockedAddUserService = {
            getCountryPhoneCodes: emptyPromise
        };

        beforeEach(function () {
            module(function ($provide) {
                $provide.value('usersAddUserService', mockedAddUserService);
            });
        });

        var scope;
        beforeEach(inject(function ($rootScope, $q, $controller) {
            scope = $rootScope.$new();
            $controller('usersAddUserController', {
                $scope: scope
            });
        }));

        it('should call getCountryPhoneCodes method on init', function () {
            spyOn(mockedAddUserService, 'getCountryPhoneCodes').and.callThrough();

            scope.$root.$digest();

            expect(mockedAddUserService.getCountryPhoneCodes).toHaveBeenCalled();
        });

    });
}());
Share:
31,133
Thomas Weglinski
Author by

Thomas Weglinski

Software Engineer

Updated on July 09, 2022

Comments

  • Thomas Weglinski
    Thomas Weglinski almost 2 years

    I have a method defined in AngularJS controller which is called on initialization. I want to test it using Jasmine ("jasmine-core": "^2.3.4", "karma": "^0.12.37"). I follow some tutorials on the Internet and StackOverflow questions, but I cannot find the right answer. Please take a look at this code:

    Controller usersAddUserController:

    (function () {
        'use strict';
    
        angular.module('app.users.addUser')
            .controller('usersAddUserController', ['$scope', 'usersAddUserService', function ($scope, usersAddUserService) {
    
                usersAddUserService.getCountryPhoneCodes().then(function (phoneCodes) {
                    $scope.phoneCodes = phoneCodes;
                });
    
            }]);
    }());
    

    Jasmine test:

    (function () {
    
        'use strict';
    
        describe('usersAddUserControllerUnitTest', function () {
            var scope, deferred, objectUnderTest, mockedAddUserService;
    
            beforeEach(module('app'));
    
            beforeEach(inject(function ($rootScope, $q, $controller) {
                scope = $rootScope.$new();
    
                function emptyPromise() {
                    deferred = $q.defer();
                    return deferred.promise;
                }
    
                mockedAddUserService = {
                    getCountryPhoneCodes: emptyPromise
                };
                         
                objectUnderTest = $controller('usersAddUserController', {
                    $scope: scope,
                    usersAddUserService: mockedAddUserService
                });
            }));
    
            it('should call getCountryPhoneCodes method on init', function () {
                //when            
                spyOn(mockedAddUserService, 'getCountryPhoneCodes').and.callThrough();
                
                deferred.resolve();
    
                scope.$root.$digest();
                
                //then
                expect(mockedAddUserService.getCountryPhoneCodes).toHaveBeenCalled();
            });
    
        });
    }());
    

    After running the tests, the error message is:

    PhantomJS 1.9.8 (Windows 7 0.0.0) usersAddUserControllerUnitTest should call getCountryPhoneCodes method on init FAILED

        Expected spy getCountryPhoneCodes to have been called.
    

    I obviously missing something, but I cannot figure out what it is. Any help will be appreciated.

  • Thomas Weglinski
    Thomas Weglinski almost 9 years
    Thank you for the answer, but could you provide complete code? When I include your solution in my code I get the: "TypeError: 'undefined' is not an object (evaluating 'deferred.resolve')".
  • Thomas Weglinski
    Thomas Weglinski almost 9 years
    Thank you for the answer, but unfortunately still gets the same error with your code. Is it working for you?
  • Martin
    Martin almost 9 years
    I didn't actually try it, it just looked like the obvious issue. Can you put a breakpoint on your controller constructor and see if it is actually the spy it is calling?
  • Thomas Weglinski
    Thomas Weglinski almost 9 years
    Thank you very much guys! Indeed @juunas was right, I got the SUCCESS now. If juunas agree, I will accept Martin answer anyway, it helps me a lot :)
  • timtos
    timtos almost 9 years
    Two reasons why I don't like the solution that much: 1. You have to call makeController(); on every test (this could be put in a beforeEach) 2. The code does not look as straight forwared as the "provide approach" but ok, this is just a question of taste. :) And well, it seems that you have to be careful about the order of things. I don't think you have this problem with provide...
  • Martin
    Martin almost 9 years
    @timtos your points are valid. This solution is the shortest path from the original code and a working test.
  • Thomas Weglinski
    Thomas Weglinski almost 9 years
    Thank you for the code, I will try your solution as well and give a feedback what is need to be done more.