Pramodh
Pramodh

Reputation: 186

How to unit test (using Jasmine) a function in a controller which calls a factory service which returns a promise

In the below SampleController, how do I unit test that postAttributes function calls sampleService.updateMethod. I'm having trouble since the updateMethod returns promise.

    angular.module('sampleModule')
       .controller('SampleController', SampleController);

    SampleController.$inject =['sampleService'];

    function SampleController(sampleService){

    this.postAttributes = function() {    
        sampleService.updateMethod(number,attributes)
            .then(function(response){
                //do something on successful update
            },function(response){
                //do something on unsuccessful update
            });
        }; 

    }

Here is the factory service that I have:

    angular.module('sampleModule')
        .factory('sampleService', sampleService);

    sampleService.$inject = ['$http'];

    function sampleService($http) {
        return {
            getMethod: function(acctNumber){
                return $http({
                    method: 'GET',
                    url: //api endpoint
                });
            },
            updateMethod: function(number, attributes){
                return $http({
                    method: 'PUT',
                    url: //api endpoint,
                    data: //payload
                });
            }
        };
    }

I would like to mock the factory service in the controller spec rather than injecting the actual service directly into $controller, since most of unit testing guidelines specify to test a unit under isolation.

Sample controller spec:

    describe('SampleController Test', function(){
        var $controller;
        var service;

        beforeEach(angular.mock.module('sampleModule')); 

        beforeEach(angular.mock.inject(function(_$controller_){
            $controller = _$controller_;
        }));

        it('Testing $scope variable', function(){
            var sampleController = $controller('SampleController', { 
                sampleService: service, //mocked factory service 
            });

            sampleController.postAttributes(); //calling the function first
            //here I would like to make an assertion to check if
            //sampleService.updateMethod has been called with certain parameters                        
            //how do I do that??
        });

    });

Upvotes: 4

Views: 6413

Answers (2)

Pramodh
Pramodh

Reputation: 186

Googled around and found a solution to mock the factory service and followed promise creation approach like @TehBeardedOne and made it to return it from the mocked service.

    describe('SampleController', function(){

        var mockService, controller, deferred, $rootScope;

        beforeEach(function(){

            angular.mock.module('sampleModule');

            angular.mock.module(function($provide){
                $provide.factory('sampleService', function($q){
                    function updateMethod(acct, attr){
                        deferred = $q.defer();
                        return deferred.promise;
                    }
                    return{updateMethod: updateMethod};
                });
            });

            angular.mock.inject(function($controller, sampleService, _$rootScope_){
                $rootScope = _$rootScope_;
                mockService = sampleService;
                spyOn(mockService, 'updateMethod').and.callThrough();
                controller =$controller('SampleController', {
                    sampleService: mockService,
                })
            });
        });

        it('postAttributes function should call updateMethod', function(){

            controller.postAttributes();
            expect(mockService.updateMethod).toHaveBeenCalled();
            expect(mockService.updateMethod).toHaveBeenCalledWith(controller.accountNumber, controller.attributes);
        });

        it('postAttributes success block', function(){
            controller.postAttributes();
            var res = {
                data: '2323'
            }
            deferred.resolve(res);
            $rootScope.$digest();
            expect(//something in success block).toBe(something);
        });

        it('postAttributes failure block', function(){
            controller.postAttributes();
            var res = {
                data: '9898'
            }
            deferred.reject(res);
            $rootScope.$digest();
            expect(controller.lame).toBe('dont type shit');
        });

    });

I've mocked the sampleService with $provider service and made updateMethod to return a promise using $q. Later on you can resolve or reject the promise and test the success and failure blocks in individual it blocks.

Upvotes: 3

tehbeardedone
tehbeardedone

Reputation: 2858

You will need to use a spy to check that the function is being called. There are several ways to go about doing this as I'm sure you have probably already noticed. You also need to inject $q and $rootScope as you will need to loop the digest cycle in order to return the promise.

In fact, if all you want to do is check that the function is being called a simple spy will work just fine. You don't even have to return the promise. That being said, if you want to continue on through the postAttributes() function and test other stuff inside this function then you will need to return the promise to do that. Here is the approach that works well for me. Something like this should work.

describe('SampleController Test', function(){
    var $controller;
    var service;
    var $q;
    var $rootScope;
    var updateMethodDeferred;
    var ctrl;
    var mockHttpObject;

    beforeEach(angular.mock.module('sampleModule'));

    beforeEach(angular.mock.inject(function(_sampleService_, _$q_, _$rootScope_, _$controller_){
        service = _sampleService_;
        $q = _$q_;
        $rootScope = _$rootScope_;
        $controller = _$controller_;

        mockHttpObject = {
            //add mock properties here
        }

        updateMethodDeferred = $q.defer();
        spyOn(service, 'updateMethod').and.returnValue(updateMethodDeferred.promise);

        ctrl = $controller('SampleController', {
            sampleService: service, //mocked factory service
        });
    }));

    it('Testing $scope variable', function(){
        ctrl.postAttributes();
        expect(service.updateMethod).toHaveBeenCalled();
    });

    it('Should do something after promise is returned', function(){
        ctrl.postAttributes();
        updateMethodDeferred.resolve(mockHttpObject);
        $rootScope.$digest();
        expect(/*something that happened on successful update*/).toBe(/*blah*/)           
    });
});

Upvotes: 1

Related Questions