Mocking function that returns promise

Promises are very useful to develope non-blocking web applications and it also helps to avoid pyramid of doom.

AngularJS supports Q based promises, this means, we can create functions in AngularJS application that uses promises. When it comes to unit testing functions that returns promises, it is just like unit testing normal functions only if you remember simulate scope's life cycle using $apply() or $digest()

Let's see this with an example. The following controller loads restaurants using promise

// homeController
(function () {
    'use strict';

    angular
        .module('app')
        .controller('homeController', ['restaurantService', homeController]);

    function homeController(restaurantService) {

        // #region viewmodel
        var vm = this;
        vm.restaurants = [];

        // #endregion

        // #region activate
        activate();

        function activate() {
            getRestaurants();
        }
        // #endregion

        // #region internal methods

        function getRestaurants() {
            restaurantService.getRestaurants()
                .then(function (data) {
                    vm.restaurants = data;
                })
                .catch(function (error) {
                    // error
                });
        }

        // #endregion
    }

})();

The unit testing for this method would look like this

// unit test
describe('home page', function () {

    var $controller;
    var $q;
    var restaurantService;

    beforeEach(function () {

        // load module
        module('app');

        // overrides for mock injections
        module(function ($provide) {
            // override any dependency here
            // $provide.value('service', 'override'); 

        });

        // initialise
        inject(function (_$controller_, _$q_, _restaurantService_) {
            $controller = _$controller_;
            $q = _$q_;
            restaurantService = _restaurantService_;
        });
    });

    describe('when home controller is initiated', function () {

        it('should load restaurants', function () {

            restaurantService.getRestaurants = function() { return $q.when(['rest1', 'rest2']); }
            var target = $controller('homeController', { restaurantService: restaurantService });

            expect(target.restaurants).toEqual(['rest1', 'rest2']);
        });

    });
});

When you run this unit test, it returns error stating that [] is not equal to ['rest1', 'rest2']. This is because callback functions are processed as part of $evalAsync in $digest loop. Unit tests are not in Angular's execution context to run $digest loop automatically so it needs some help to process the callbacks. All we need to do to make it work is to invoke $digest method on controller's scope or any of its parent.

Here is the updated example,

// unit test
describe('home page', function () {

    var $controller;
    var $q;
    var $rootScope;
    var restaurantService;

    beforeEach(function () {

        // load module
        module('app');

        // overrides for mock injections
        module(function ($provide) {
            // override any dependency here
            // $provide.value('service', 'override'); 

        });

        // initialise
        inject(function (_$controller_, _$q_, _$rootScope_, _restaurantService_) {
            $controller = _$controller_;
            $q = _$q_;
            $rootScope = _$rootScope_;
            restaurantService = _restaurantService_;
        });
    });

    describe('when home controller is initiated', function () {

        it('should load restaurants', function () {

            restaurantService.getRestaurants = function() { return $q.when(['rest1', 'rest2']); }
            var target = $controller('homeController', { restaurantService: restaurantService });
            $rootScope.$digest();

            expect(target.restaurants).toEqual(['rest1', 'rest2']);
        });

    });
 });

You can find more information on how $digest loop works here.

comments powered by Disqus