JavaScript
Article

Unit Testing in AngularJS: Services, Controllers & Providers

By Ravi

angularjs

AngularJS is designed with testability in mind. Dependency injection is one of the prominent features of the framework that makes unit testing easier. AngularJS defines a way to neatly modularize the application and divide it into different components such as controllers, directives, filters or animations. This model of development means that the individual pieces work in isolation and the application can scale easily over a long period of time. As extensibility and testability go hand-in-hand, it is easy to test AngularJS code.

As per the definition of unit testing, the system under test should be tested in isolation. So, any external objects needed by the system have to be replaced with mock objects. As the name itself says, the mock objects do not perform an actual task; rather they are used to meet the expectations of the system under test. If you need a refresher on mocking, please refer to one of my previous articles: Mocking Dependencies in AngularJS Tests.

In this article, I will share a set of tips on testing services, controllers and providers in AngularJS. The code snippets have been written using Jasmine and can be run with the Karma test runner. You can download the code used in this article from our GitHub repo, where you will also find instructions on running the tests.

Testing Services

Services are one of the most common components in an AngularJS application. They provide a way to define re-usable logic in a central place so that one doesn’t need to repeat the same logic over and over. The singleton nature of the service makes it possible to share the same piece of data across multiple controllers, directives and even other services.

A service can depend on a set of other services to perform its task. Say, a service named A depends on the services B, C and D to perform its task. While testing the service A, the dependencies B, C and D have to be replaced with mocks.

We generally mock all the dependencies, except certain utility services like $rootScope and $parse. We create spies on the methods that have to be inspected in the tests (in Jasmine, mocks are referred to as spies) using jasmine.createSpy() which will return a brand new function.

Let’s consider the following service:

angular.module('services', [])
  .service('sampleSvc', ['$window', 'modalSvc', function($window, modalSvc){
    this.showDialog = function(message, title){
      if(title){
        modalSvc.showModalDialog({
          title: title,
          message: message
        });
      } else {
        $window.alert(message);
      }
    };
  }]);

This service has just one method (showDialog). Depending on the value of the input this method receives, it calls one of two services that are injected into it as dependencies ($window or modalSvc).

To test sampleSvc we need to mock both of the dependent services, load the angular module that contains our service and get references to all the objects:

var mockWindow, mockModalSvc, sampleSvcObj;
beforeEach(function(){
  module(function($provide){
    $provide.service('$window', function(){
      this.alert= jasmine.createSpy('alert');
    });
    $provide.service('modalSvc', function(){
      this.showModalDialog = jasmine.createSpy('showModalDialog');
    });
  });
  module('services');
});

beforeEach(inject(function($window, modalSvc, sampleSvc){
  mockWindow=$window;
  mockModalSvc=modalSvc;
  sampleSvcObj=sampleSvc;
}));

Now we can test the behavior of the showDialog method. The two test cases we can write for the method are as follows:

  • it calls alert if no title is parameter is passed in
  • it calls showModalDialog if both title and message parameters are present

The following snippet shows these tests:

it('should show alert when title is not passed into showDialog', function(){
  var message="Some message";
  sampleSvcObj.showDialog(message);

  expect(mockWindow.alert).toHaveBeenCalledWith(message);
  expect(mockModalSvc.showModalDialog).not.toHaveBeenCalled();
});

it('should show modal when title is passed into showDialog', function(){
  var message="Some message";
  var title="Some title";
  sampleSvcObj.showDialog(message, title);

  expect(mockModalSvc.showModalDialog).toHaveBeenCalledWith({
    message: message,
    title: title
  });
  expect(mockWindow.alert).not.toHaveBeenCalled();
});

This method doesn’t have a lot of logic to test, whereas the services in typical web apps would normally contain a lot of functionality. You can use the technique demonstrated in this tip for mocking and getting the references to services. The service tests should cover every possible scenario that was assumed while writing the service.

Factories and values can also be tested using the same technique.

Testing Controllers

The set-up process for testing a controller is quite different from that of a service. This is because controllers are not injectable, rather they are instantiated automatically when a route loads or, an ng-controller directive is compiled. As we don’t have the views loading in tests, we need to manually instantiate the controller under test.

As the controllers are generally tied to a view, the behavior of methods in the controllers depends on the views. Also, some additional objects may get added to the scope after the view has been compiled. One of the most common examples of this is a form object. In order to make the tests work as expected, these objects have to be manually created and added to the controller.

A controller can be of one of the following types:

  • Controller used with $scope
  • Controller used with Controller as syntax

If you’re not sure on the difference, you can read more about it here. Either way, we will discuss both of these cases.

Testing Controllers with $scope

Consider the following controller:

angular.module('controllers',[])
  .controller('FirstController', ['$scope','dataSvc', function($scope, dataSvc) {
    $scope.saveData = function () {
      dataSvc.save($scope.bookDetails).then(function (result) {
        $scope.bookDetails = {};
        $scope.bookForm.$setPristine();
      });
    };

    $scope.numberPattern = /^\d*$/;
  }]);

To test this controller, we need to create an instance of the controller by passing in a $scope object and a mocked object of the service (dataSvc). As the service contains an asynchronous method, we need to mock that using the mocking promise technique I outlined in a previous article.

The following snippet mocks the dataSvc service:

module(function($provide){
  $provide.factory('dataSvc', ['$q', function($q)
    function save(data){
      if(passPromise){
        return $q.when();
      } else {
        return $q.reject();
      }
    }
    return{
      save: save
    };
  }]);
});

We can then create a new scope for the controller using the $rootScope.$new method. After creating an instance of the controller, we have all the fields and methods on this new $scope.

beforeEach(inject(function($rootScope, $controller, dataSvc){
  scope=$rootScope.$new();
  mockDataSvc=dataSvc;
  spyOn(mockDataSvc,'save').andCallThrough();
  firstController = $controller('FirstController', {
    $scope: scope, 
    dataSvc: mockDataSvc
  });
}));

As the controller adds a field and a method to $scope, we can check if they are set to right values and if the methods have the correct logic. The sample controller above adds a regular expression to check for a valid number. Let’s add a spec to test the behavior of the regular expression:

it('should have assigned right pattern to numberPattern', function(){
    expect(scope.numberPattern).toBeDefined();
    expect(scope.numberPattern.test("100")).toBe(true);
    expect(scope.numberPattern.test("100aa")).toBe(false);
});

If a controller initializes any objects with default values, we can check their values in the spec.

To test the saveData method, we need to set some values for the bookDetails and bookForm objects. These objects would be bound to UI elements, so are created at runtime when the view is compiled. As already mentioned, we need to manually initialize them with some values before calling the saveData method.

The following snippet tests this method:

it('should call save method on dataSvc on calling saveData', function(){
    scope.bookDetails = {
      bookId: 1, 
      name: "Mastering Web application development using AngularJS", 
      author:"Peter and Pawel"
    };
    scope.bookForm = {
      $setPristine: jasmine.createSpy('$setPristine')
    };
    passPromise = true;
    scope.saveData();
    scope.$digest();
    expect(mockDataSvc.save).toHaveBeenCalled();
    expect(scope.bookDetails).toEqual({});
    expect(scope.bookForm.$setPristine).toHaveBeenCalled();
});

Testing Controllers with ‘Controller as’ Syntax

Testing a controller which uses the Controller as syntax is easier than testing the one using $scope. In this case, an instance of the controller plays the role of a model. Consequently, all actions and objects are available on this instance.

Consider the following controller:

angular.module('controllers',[])
  .controller('SecondController', function(dataSvc){
    var vm=this;

    vm.saveData = function () {
      dataSvc.save(vm.bookDetails).then(function(result) {
        vm.bookDetails = {};
        vm.bookForm.$setPristine();
      });
    };

    vm.numberPattern = /^\d*$/;
  });

The process of invoking this controller is similar to the process discussed earlier. The only difference is, we don’t need to create a $scope.

beforeEach(inject(function($controller){
  secondController = $controller('SecondController', {
    dataSvc: mockDataSvc
  });
}));

As all members and methods in the controller are added to this instance, we can access them using the instance reference.

The following snippet tests the numberPattern field added to the above controller:

it('should have set pattern to match numbers', function(){
  expect(secondController.numberPattern).toBeDefined();
  expect(secondController.numberPattern.test("100")).toBe(true);
  expect(secondController.numberPattern.test("100aa")).toBe(false);
});

Assertions of the saveData method remain the same. The only difference in this approach is with the way we initialize values to the bookDetails and bookForm objects.

The following snippet shows the spec:

it('should call save method on dataSvc on calling saveData', function () 
  secondController.bookDetails = {
    bookId: 1,
    name: "Mastering Web application development using AngularJS",
    author: "Peter and Pawel"
  };
  secondController.bookForm = {
    $setPristine: jasmine.createSpy('$setPristine')
  };
  passPromise = true;
  secondController.saveData();
  rootScope.$digest();
  expect(mockDataSvc.save).toHaveBeenCalled();
  expect(secondController.bookDetails).toEqual({});
  expect(secondController.bookForm.$setPristine).toHaveBeenCalled();
});

Testing Providers

Providers are used to expose an API for application-wide configuration that must be made before the application starts. Once the configuration phase of an AngularJS application is over, interaction with providers is disallowed. Consequently, providers are only accessible in config blocks, or other provider blocks. We cannot obtain a provider instance using an inject block, rather we need to pass a callback to the module block.

Let’s consider the following provider which depends on a constant (appConstants) a second provider (anotherProvider):

angular.module('providers', [])
  .provider('sample', function(appConstants, anotherProvider){

    this.configureOptions = function(options){
      if(options.allow){
        anotherProvider.register(appConstants.ALLOW);
      } else {
        anotherProvider.register(appConstants.DENY);
      }
    };

    this.$get = function(){};
  });

In order to test this, we first need to mock the dependencies. You can see how to do this in the sample code.

Before testing the provider, we need to ensure that the module is loaded and ready. In tests, loading of the modules is deferred till an inject block is executed or, the first test is executed. In a couple of projects, I have seen some tests which use an empty first test to load the module. I am not a fan of this approach as the test doesn’t do anything and adds a count to your total number of tests. Instead, I use an empty inject block to get the modules loaded.

The following snippet gets the references and loads the modules:

beforeEach(module("providers"));
beforeEach(function(){
  module(function(anotherProvider, appConstants, sampleProvider){
    anotherProviderObj=anotherProvider;
    appConstantsObj=appConstants;
    sampleProviderObj=sampleProvider;
  });
});
beforeEach(inject());

Now that we have all of the references, we can call methods defined in the providers and test them:

it('should call register with allow', function(){
  sampleProviderObj.configureOptions({allow:true});
  expect(anotherProviderObj.register).toHaveBeenCalled();
  expect(anotherProviderObj.register).toHaveBeenCalledWith(appConstantsObj.ALLOW);
});

Conclusion

Unit testing becomes tricky at times, but it is worth spending the time on it as it ensures the correctness of the application. AngularJS makes it easier to unit test the code written using the framework. I hope this article gives you enough idea to expand and enhance the tests in your applications. In a future article we will continue looking at how to test other pieces of your code.

Comments
GregY

This is a fantastic article that is incredibly helpful, but I can't get the github to link to work. I would like to see the tests all put together so I can verify what I am doing is right. Keep up the good work!

Pullo

Hi GregY,

Thanks for spotting the broken link.
The link actually points where it should, but our repo seems to have vanished.
I'll look into it and get it fixed soon.

Pullo

Should be fixed now.

GregY

Excellent it is working perfectly now. Thank you very much!

gsans

Cool. Thanks for sharing! Modal example was great.

If you want to see other unit testing examples for Controllers, Services, Directives, Filters, Routes, Promises and Events. You can find them in this plunker http://plnkr.co/edit/4SSXcRLdPqgb0R3jMB7Z?p=preview

Yuripetusko

This helped me a lot. Thanks

Kogratte

expect(mockModalSvc.showModalDialog).toHaveBeenCalled();
expect(mockModalSvc.showModalDialog).toHaveBeenCalledWith({
message: message,
title: title
});

How mockModalSvc.showModalDialog should have been called with parameters if the previous assert has failed? You do not need to use hasBeenCalled followed by hasBeenCalledWith. Simply use hasBeenCalledWith: if the spyed method was never used, it would fail. If it was cold without provided arguments, it would fail.

sravikiran

Kogratte,

You are correct. As toHaveBeenCalledWith checks for calling and also parameters, we can drop the first check. I have been checking both of them just out of habit. Thanks for pointing.

Pullo

@Kogratte: As Ravi says, thanks for pointing this out. I updated the article to remove the superfluous checks.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.