AngularJS Abstractions: Services

At first glance, services in AngularJS are a catch-all destination for any code that doesn’t fall into one of the other primary abstractions in the framework. If it’s not a controller, filter, directive, or model,  it must be a service. One reason to think to think this way is because one can interpret the word “service” to mean anything in software. However, services in Angular have capabilities and behaviors that make them well suited for specific jobs.

Services As Application Helpers

Controllers, applications, and even other services can take a dependency on 0 or more services, meaning services can be a convenient location for code that is used in different places throughout an application. For example, if two controllers both require some algorithmic logic to make a decision, it would be better to place the logic inside a service that both controllers can use than have the code duplicated in two different controllers.

Services As Singletons

AngularJS will manage services to make sure there is only one instance of a service per application. This makes services a convenient storage location for data that needs to stick around (controllers and their models can come and go as views are loaded and unloaded in the DOM).

Services As Communication Hubs

AngularJS provides a framework to achieve a separation of concerns and a the various components have no knowledge of each other. Since services can be injected into almost anything (controllers, directives, filters, and even other services), a service can play the role of a mediator or event aggregator for other components to communicate in a loosely coupled manner.

Services As Injectable Dependencies

Perhaps the biggest reason to put code into a service is because services are injected into other components, meaning components maintain a loose coupling and any particular service can be replaced during a unit test. AngularJS itself provides many services that fall into this category. There is the $http service (for network communication),  the $window service (a wrapper for the global window object), the $log service, and others.

What Isn’t A Service?

There is plenty of vanilla JavaScript code you can write for an AngularJS application that doesn’t have to live inside an AngularJS abstraction. Model and view model definitions, for example, could live outside of any controller or service. Controllers could instantiate models, obviously, and services could hold references and vice versa. My candidates for services generally fall into one of the previous 4 categories, and as models are easy to test without test doubles, they don’t need to be injected and hence don’t need to be considered a service themselves, although it is certainly reasonable to make a service responsible for fetching a model from some unknown data source.

How To Create A Service?

One of the confusing areas of AngularJS revolves around the APIs for  creating a service, since there are no fewer than 6 ways to register a service and the obvious API choice, a method named service, probably shouldn’t be your first choice. The one that can adapt to just about any scenario is the factory method, which when used on a module object looks like something that would define a factory function for the module, but in fact is defining a factory function for a service (another unfortunate point of confusion).

As an example, here is a service to wrap the geolocation APIs of  the browser:

(function () {
 
    var geo = function ($q, $rootScope) {
 
        var apply = function () {
            $rootScope.$apply();
        };
 
        var locate = function () {
            var defer = $q.defer(); 
            navigator.geolocation.getCurrentPosition(
                function (position) {
                    defer.resolve(position);
                    apply();
                },
                function(error) {
                    defer.reject(error);
                    apply();
                });
            return defer.promise;
        };
 
        return {
            locate: locate
        };
    };
 
    geo.$inject = ["$q", "$rootScope"];
 
    angular.module("testApp")
           .factory("geo", geo);
 
}());

The factory method takes a function that AngularJS will invoke to create the service instance. This geo service makes use of the $q service provided by Angular to create promises for the asynchronous delivery of latitude and longitude. Somewhere else in the app, a controller can take a dependency on the geo service and use it to find the user’s position.

(function () {
 
    var TestController = function ($scope, geo) {
 
        geo.locate()
           .then(function (position) {
                $scope.position = position;
            });
    };
 
    TestController.$inject = ["$scope", "geo"];
 
    angular.module("testApp")
           .controller("TestController", TestController);
 
}());

Summary

Services are an important part of any AngularJS application. The built-in services in the framework make code more testable, and custom services can provide an abstraction to make code easier to maintain.

AngularJS Tests With An HTTP Mock

 Let’s look at a controller that likes to communicate over the network.

(function (module) {
 
    var MoviesController = function ($scope, $http) {
 
        $http.get("/api/movies")
            .then(function (result) {
                $scope.movies = result.data;
            });
    };
 
    module.controller("MoviesController",
        ["$scope", "$http", MoviesController]);
 
}(angular.module("myApp")));

If you want a unit test to execute without network communication there are a couple options. You could refactor the controller to use a custom service to fetch movies instead of using $http directly. Then in a unit test it would be easy to provide a fake service that returns pre-canned responses.

Another option is to use angular-mocks. Angular mocks includes a programmable fake $httpBackend that replaces the real $httpBackend service. It is important to note that $httpBackend is not the same as the $http service. The $http service uses $httpBackend to send HTTP messages. In a unit test, we’ll use the real $http service, but a fake $httpBackend programmed to respond in a specific way.

Here is one approach:

describe("myApp", function () {
 
    beforeEach(module('myApp'));
 
    describe("MoviesController", function () {
 
        var scope, httpBackend;
        beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) {
            scope = $rootScope.$new();
            httpBackend = $httpBackend;
            httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]);
            $controller('MoviesController', {
                $scope: scope,
                $http: $http
            });
        }));
 
        it("should have 3 movies", function () {
            httpBackend.flush();
            expect(scope.movies.length).toBe(3);
        });
    });
});

In the above test, $httpBackend is programmed (with the when method) to respond to a GET request with three objects (empty objects, since the controller in this example never uses the data, but only assigns the response to the model).

For the data to arrive in the model, the test needs to call the flush method. Flush will respond to all requests with the programmed responses. The flush method will also verify there are no outstanding expectations. Writing tests with expectations is a little bit different.

describe("myApp", function () {
 
    beforeEach(module('myApp'));
 
    describe("MoviesController", function () {
 
        var scope, httpBackend, http, controller;
        beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) {
            scope = $rootScope.$new();
            httpBackend = $httpBackend;
            http = $http;
            controller = $controller;
            httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]);
        }));
       
        it('should GET movies', function () {
            httpBackend.expectGET('/api/movies');
            controller('MoviesController', {
                $scope: scope,
                $http: http
            });
            httpBackend.flush();
        });
    });
});

The expectation in the above code is registered with the expectGET method. When the test calls flushflush will fail the test if the controller did not make all of the expect calls. Of the two tests in this post, I prefer the first style. Testing with expectations is the same as interaction testing with mocks, which I’ve learned to shy away from because interaction testing tends to be brittle.

Simple Unit Tests With AngularJS

For those of you that have company sites that still use AngularJs 1.3 - 1.6, heres a short help on how to unit test your code.

One of the benefits of using AngularJS is the ability to unit test the JavaScript code in a complex application. Unit testing is incredibly easy for trivial cases when controllers and models are declared in global scope. However unit testing is slightly more challenging for objects defined inside of Angular modules because of the need to bootstrap modules, work with a dependency injector, and deal with the subtleties of nested functional code. 

Let’s try to test the following controller defined in a module:

(function (app) {
     
    var SimpleController = function ($scope) {
      
        $scope.x = 3;
        $scope.y = 4;
        $scope.doubleIt = function () {
            $scope.x *= 2;
            $scope.y *= 2;
        };
    };
     
    app.controller("SimpleController", 
             ["$scope", SimpleController]);
     
}(angular.module("myApp")));

We’ll be using AngularJS mocks and Jasmine in an HTML page, which requires the following scripts:

- jasmine.js

- jasmine-html.js

- angular.js

- angular-mocks.js

- simpleController.js (where the controller lives)

It’s important to include the Jasmine scripts before including angular-mocks, as angular-mocks will enable some additional features when Jasmine is present (notably the helper methods module and inject).

describe("myApp", function() {
 
    beforeEach(module('myApp'));
 
    describe("SimpleController", function() {
 
        var scope;
        beforeEach(inject(function($rootScope, $controller) {
            scope = $rootScope.$new();
            $controller("SimpleController", {
                $scope: scope
            });
        }));
 
        it("should double the numbers", function() {
            scope.doubleIt();
            expect(scope.x).toBe(6);
        });
    });
});