When unit testing AngularJS code using Jasmine, there is a lot of duplicate AngularJS unit test plumbing code. Even when using beforeEach methods, you could have multiple spec files across your site so the chance for duplicating code increases.

For example, consider what it takes to set up an AngularJS unit test for a controller with scope:

beforeEach(function () {
    var controller, scope;

    module('app');
    inject(function (_$controller_, _$rootScope_) {
        $controller = _$controller_;
        $rootScope = _$rootScope_;
        scope = $rootScope.$new();

        controller = $controller('myController', {
            $scope: scope
        });
    });
});

That’s a lot of code for each of your controllers!  It would be nice if this type of code could be placed in a service so that it could be reused for all spec files for the site.  That’s exactly what I’d like to propose in this article.

Source and Demo

Source code:  https://github.com/IntertechInc/angular-test-module

Demo:  http://plnkr.co/edit/MDGVqgrpiQEAu6bVhBSL?p=preview

CONTROLLER TESTS

The mockServiceModule that is available at the above GitHub site is meant as a starting place for the AngularJS unit test in your project so that you can focus on the tests and not the plumbing to get them working.  Add and remove code at will, the idea is to eliminate duplicate code as much as possible.

CONTROLLER CODE TO BE TESTED

The following code is the AngularJS controller that we want to test:

(function () {
    'use strict';

    angular
        .module('app')
        .controller('testController', testController);

    testController.$inject = ['$scope', 'temperatureService'];

    function testController($scope, temperatureService) {
        $scope.model = {};

        $scope.toCelsius = function () {
            temperatureService.toCelsius($scope.model.fahrenheit)
            .then(function (response) {
                $scope.model.celsiusResult = 'Celsius: ' + response.data.value;
            });
        }

        $scope.toFahrenheit = function () {
            temperatureService.toFahrenheit($scope.model.celsius)
            .then(function (response) {
                $scope.model.fahrenheitResult = 'Fahrenheit: ' + response.data.value;
            });
        }
    }
})();

Notes:

  • The temperatureService calls an API to convert Fahrenheit to Celsius and vice versa
  • We will test that the celsiusResult and fahrenheitResult are set properly
  • No error handling is being considered for this test

BEFOREEACH TEST

Now to the AngularJS unit test.  Add the following code in beforeEach blocks (which are run before each ‘it’ function):

describe('testController', function () {
    var mockSvc;

    beforeEach(module('app'));
    beforeEach(module('mockServiceModule'));

    beforeEach(function () {
        module(function ($provide, mockServiceProvider) {
            mockServiceProvider.mockTemperatureService($provide);
        });

        inject(function (mockService) {
            mockSvc = mockService;
            mockSvc.configureTemperatureServiceMock();

            mockSvc.getController()('testController', {
                '$scope': mockSvc.getScope(),
                'temperatureService': mockSvc.getTemperatureServiceMock()
            });
        });
    });

Notes:

  • Add the ‘app’ module first
  • Add the ‘mockServiceModule’ after the ‘app’
  • In the final beforeEach, add a module (if necessary) for any custom configuration for the mockServiceProvider
    • In the example above, I’m calling mockTemperatureService, which replaces the mock for temperatureService whenever it is injected during the test
    • This is good for the controller, but if we want to test the code in the temperatureService, we wouldn’t want to do this
  • In the inject function, add any services you’ll need, including the mockService (which is the service that the mockServiceProvider creates)
    • Now is a good time to configure things that require services
    • Use mockSvc to call getController and getScope – no more of this plumbing code, it’s all done for you in the mockService

UNIT TESTS FOR CONTROLLER

Now you are ready to run some unit tests with the mockService.

it('should set celsiusResult when toCelsius called', function () {
    // Arrange
    var expectedValue = '0';

    var scope = mockSvc.getScope();
    scope.model = { fahrenheit: 32 };

    // Act
    scope.toCelsius();

    mockSvc.resolveTSMock(true, expectedValue);
    scope.$apply();

    // Assert
    expect(scope.model.celsiusResult).toBe('Celsius: ' + expectedValue);
});

it('should set fahrenheitResult when toFahrenheit called', function () {
    // Arrange
    var expectedValue = '32';

    var scope = mockSvc.getScope();
    scope.model = { celsius: 0 };

    // Act
    scope.toFahrenheit();

    mockSvc.resolveTSMock(false, expectedValue);
    scope.$apply();

    // Assert
    expect(scope.model.fahrenheitResult).toBe('Fahrenheit: ' + expectedValue);
});

Notes:

  • Use mockSvc to get the scope
  • Call the controller method toCelsius/toFahrenheit
  • Use mockSvc to resolve the temperatureServiceMock toCelsius/toFahrenheit promise with the given value
  • Now model.celsiusResult/fahrenheitResult should be set so assert that it’s correct using ‘expect’

MOCK $HTTP CALLS

The controller that was tested previously doesn’t have to mock the $http service since it mocked the temperatureService and it contains the calls to the $http service.  So let’s take a look at what it will take to mock the $http service using the mockService by testing the temperatureService itself.

SERVICE CODE TO TEST

This is the temperatureService:

(function () {
    'use strict';

    angular
        .module('app')
        .factory('temperatureService', temperatureService);

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

    function temperatureService($http) {
        var service = {
            toCelsius: fahrenheitToCelsius,
            toFahrenheit: celsiusToFahrenheit
        };

        return service;

        function fahrenheitToCelsius(f) {
            return $http.get('http://private-81ea-temperatureconverter.apiary-mock.com/toCelsius/' + f);
        }

        function celsiusToFahrenheit(c) {
            return $http.get('http://private-81ea-temperatureconverter.apiary-mock.com/toFahrenheit/' + c);
        }
    }
})();

Notes:

  • There are 2 functions in this service:  toCelsius and toFahrenheit
  • Using $http.get to call an API that I created using Apiary
    • If you try to call this API, toCelsius will always return 0 and toFahrenheit will always return 32

ANgularJS UNIT TEST SETUP

This is how to setup the unit tests for the service:

describe('temperatureService', function () {
    var temperatureSvc, mockSvc;

    beforeEach(module('app'));
    beforeEach(module('mockServiceModule'));

    beforeEach(function () {
        inject(function (temperatureService, mockService) {
            mockSvc = mockService;
            temperatureSvc = temperatureService;
        });
    });

Notes:

  • Notice that there is no extra module in the last beforeEach since we are not mocking the temperatureService here
  • Inject is injecting the temperatureService itself, not a mock of it

SERVICE UNIT TESTS

The service is now ready to be tested.

it('should convert 32 Fahrenheit to 0 Celsius', function () {
    // Arrange
    var f = 32, c = 0;
    mockSvc.configureTemperatureService(true, f, c);

    // Act
    temperatureSvc.toCelsius(f).then(function (response) {
        // Assert
        expect(response.data.value).toBe(c);
    });
    mockSvc.flush();
});

it('should convert 0 Celsius to 32 Fahrenheit', function () {
    // Arrange
    var f = 32, c = 0;
    mockSvc.configureTemperatureService(false, c, f);

    // Act
    temperatureSvc.toFahrenheit(c).then(function (response) {
        // Assert
        expect(response.data.value).toBe(f);
    });
    mockSvc.flush();
});

Notes:

  • Use the mockSvc to call configureTemperatureService (see code below)
  • Call toCelsius/toFahrenheit of the temperatureService and then do the assert in the success function
  • mockSvc.flush() tells the httpBackend to process promises

MOCKSERVICE CODE

Since the mockService is a starting point for you and you will undoubtedly add your own code, it is important to describe what is going on in that code.  This section aims to do just that.

MODULE DEFINITION AND INTERFACE

The following code creates the mockServiceModule, the mockService provider and defines the interface for the service:

angular
    .module('mockServiceModule', [])
    .provider('mockService', function mockServiceProvider() {
        // Public service interface.
        var service = {
            getScope: function () { return scope; }
            , getController: function () { return controller; }
            , getQ: function () { return q; }
            , getTemperatureServiceMock: function () { return temperatureServiceMock; }
            , configureTemperatureService: configureTemperatureService
            , configureTemperatureServiceMock: configureTemperatureServiceMock
            , resolveTSMock: resolveTSMock
            , flush: function () { httpBackend.flush(); }
        };

Notes:

  • ‘mockServiceModule’ is what you add to your unit test specs
  • mockService is a provider because it needs to be able to customize itself at config time
  • ‘service’ is the interface for the service, not the provider so these aren’t available at config time, only when the service is injected
  • The temperatureService code can safely be removed for your purposes
  • See Custom Functions section for explanation of functions like resolveTSMock

PROVIDER FUNCTIONS

Provider functions are those that are available at config time.  It would commonly be used to replace a real service with a mock or function.

// Anything that is common to all tests are added here.
this.initProvider = function (provide, options) {
    // Override $exceptionHandler in case it calls a service
    provide.value('$exceptionHandler', exceptionHandlerMock);
};

function exceptionHandlerMock(exception, cause) {
    if (cause) {
        exception.message += ' (caused by "' + cause + '")';
    }
    expect(exception.message).toBe('');
}

this.mockTemperatureService = function (provide) {
    temperatureServiceMock = jasmine.createSpyObj('temperatureService', ['toCelsius', 'toFahrenheit']);
    provide.value('temperatureService', temperatureServiceMock);
};

Notes:

  • initProvider is called automatically for you at the bottom of the file in the .config function
  • Use ‘this.’ instead of ‘service.’ because these are part of the provider itself
  • The ‘options’ in the initProvider function would be used to pass in anything you desire to optionally execute something in initProvider or whatever else you’d like to use it for
  • In mockTemperatureService, we could have added return values to the methods but then the user couldn’t customize what values to return

$get FUNCTION

The $get function is required by providers and should return an instance of the service that will be used (i.e. mockService, which is injected).  You may want to update this section if you require another service that isn’t in the code.

var q, scope, rootScope, controller, httpBackend;

// When mockService is created, setup things that aren't available
// at config time and return the service.
this.$get = ['$q', '$rootScope', '$controller', '$httpBackend',
    function mockServiceFactory($q, $rootScope, $controller, $httpBackend) {
        rootScope = $rootScope;
        scope = $rootScope.$new();
        controller = $controller;
        q = $q;
        httpBackend = $httpBackend;

        return service;
    }];

Notes:

  • Inject any services you want to use in this function and store them for later

CUSTOM FUNCTIONS

The following functions are custom functions that are for the service:

function configureTemperatureService(isToCelsius, requestValue, responseValue) {
    var converter = 'toFahrenheit';
    if (isToCelsius) {
        converter = 'toCelsius';
    }

    httpBackend.expect('GET', 'http://private-81ea-temperatureconverter.apiary-mock.com/' + converter + '/' + requestValue)
        .respond({ value: responseValue });
}

function configureTemperatureServiceMock() {
    tsCelsiusDefer = q.defer();
    temperatureServiceMock.toCelsius.and.returnValue(tsCelsiusDefer.promise);
    tsFahrenheitDefer = q.defer();
    temperatureServiceMock.toFahrenheit.and.returnValue(tsFahrenheitDefer.promise);
}

function resolveTSMock(isToCelsius, value) {
    if (isToCelsius) {
        tsCelsiusDefer.resolve({ data: { value: value } });
    }
    else {
        tsFahrenheitDefer.resolve({ data: { value: value } });
    }
}

Notes:

  • configureTemperatureService
    • The ‘converter’ variable is just the API method to call
    • Using httpBackend since it was set to $httpBackend in the mockService in the $get function
    • The httpBackend is expecting a GET on the given URL
      • When that URL is called, respond with the given JSON object (with a value property) since that is what the ‘real’ call does
  • configureTemperatureServiceMock
    • Use q ($q) to create defer objects and return their promise for toCelsius and toFahrenheit
    • These defer objects are used in resolveTSMock and could be used to test exceptions, etc. as well
  • resolveTSMock
    • The ‘defer’ objects were created in the configureTemperatureServiceMock code
    • The ‘resolve’ method says the promise was successfully completed
      • Recreate the JSON object the controller is expecting

MOCKSERVICE WINS

These are some of the wins that use of the mockService provides:

  • Centralized unit test code
  • Duplicate unit test code eliminated
  • Mock of $exceptionHandler which can save headaches (especially if you call an API to log the exceptions)
  • Centralized use of $httpBackend to mock $http calls
  • Simple setup
  • In it’s own module so it can be included only in the spec runner html file

CONCLUSION

Unit testing AngularJS code is critical but generates a lot of duplicate plumbing code.  Using the mockServiceModule is a good way to consolidate that code for use across your site.