unit testing controller resolvers

We all know the secret to responsive ng-views, right? Well, it isn't really a secret. I am talking about routeProvder.when resolvers for pre-loading asynchronous server data. You really don't want your view sitting empty.

resolve - {Object.< string, function >=} - An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired. The map object is: - key – {string}: a name of a dependency to be injected into the controller. - factory - {string|function}: If string then it is an alias for a service. Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before its value is injected into the controller. Be aware that ngRoute.$routeParams will still refer to the previous route within these resolve functions. Use $route.current.params to access the new route parameters, instead.

Ok, great: we have a factory function for getting some stuff from the server. On a route call to /Home, the resolver function returns some data. (This data can be a promise, in which case the promise will first be resolved before the controller is instantiated.)

(function(angular) {
    'use strict';

    var app = angular.module('app', ['ngRoute']).config([
        '$routeProvider',
        function($routeProvider) {
            $routeProvider.when('/Home/:message?', {
                templateUrl: 'Home.html',
                controller: 'home',
                resolve: {
                    data: ['$route', function ($route) {
                        return "Test String Data" + $route.current.params.message;
                    }]
                }
            }).otherwise({redirectTo: '/Home'});
        }
    ]);

    app.controller('home', [
        "$scope",
        "data",
        function($scope, data) {
            $scope.message = data;
        }
    ]);
})(window.angular);

But now I have business logic in my configurations. Routing files, in my mind, are a configuration item and should not actually contain code. Keep in mind that your resolver factory function is also injectable, as the example injects $route. As a result, it can actually contain some pretty complicated business logic for looking up records. Let's take a look at an example that pulls from the server, and join the server information with some request message.

resolve : {  
  data : ['$route', '$http', '$q', function($route, $http, $q) {
      var deffer = $q.defer();
      $http.get('/Data').then(function(result){
          deffer.resolve([result.data.Value, $route.current.params.message].join(' '));
      });

      return deffer.promise;
  }]
}

It is easy to see that, even with such a small function, we now might want to look into doing some type of unit testing. But right now, the resolver is just an anonymous function--unless you bootstrap the whole angular stack and your http, which is doable, but does make the first "it" more difficult. So can we handle this in a slightly simpler manner? Just move the resolver factory function to where it can be unit tested.

app.provider('data', function () {  
        return {
            $get: function () {
                return {
                    home: [
                        '$route',
                        '$http',
                        '$q',
                        function ($route, $http, $q) {
                            var deffer = $q.defer();
                            $http.get('/Data').then(function (result) {
                                deffer.resolve([result.data.value, $route.current.params.message].join(' '));
                            });

                            return deffer.promise;
                        }
                    ]
                }
            }
        }
    });

And now that the resolver function exists as an artifact of a provider, we can expose it inside the config function.

var app = angular.module('app', ['ngRoute']).config([  
    '$routeProvider',
    'dataProvider',
    function ($routeProvider, dataProvider) {
        var data = dataProvider.$get();
        $routeProvider.when('/Home/:message?', {
            templateUrl: 'Home.html',
            controller: 'home',
            resolve: {
                data: data.home
            }
        }).otherwise({ redirectTo: '/Home' });
    }
]);

To The Unit Test

So far, we have made the resolver factory named instead of anonymous. But the holy grail here is really the unit test of the resolver function. The full source code can be found here but the meat and potatoes is outlined in the test here. Variables prefixed with _ are, in this case, different mocks.

it('The the promise resolves to a combination of the get results and route messages', function() {  
    var result = fn(_route, _http, _q);
    expect(angular.isFunction(result.then)).toBe(true);
    result.then(function(data) {
        expect(data).toBe('test test');
    });

    _timeout.flush();
});

The result of "test test" matches the mock data being returned form the _http and _route mock objects. Naturally, this example is simplified. However, I have also done some work in which complicated async logic was executed in these functions.

Conclution

It isn't a big change to wrap the resolver function inside a provider, and the benefit of being able to test a pretty vital part of the applications is significant. We also gain the ability to re-use this resolver code since multiple resolvers can be used for a single route.