Unit Testing in AngularJS: Services, Controllers & Providers

Ravi
Ravi
Share
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.

Frequently Asked Questions (FAQs) on Unit Testing AngularJS Services, Controllers, and Providers

What is the importance of unit testing in AngularJS?

Unit testing is a crucial aspect of AngularJS development. It helps to verify the functionality of individual components, such as services, controllers, and providers, in isolation. This ensures that each component works as expected before they are integrated into the larger application. Unit testing can help identify bugs early in the development process, making them easier and less costly to fix. It also aids in maintaining code quality and enhancing the overall reliability of the application.

How can I set up a testing environment for AngularJS?

Setting up a testing environment for AngularJS involves several steps. First, you need to install necessary testing tools such as Jasmine and Karma. Jasmine is a behavior-driven development framework for testing JavaScript code, while Karma is a test runner that executes tests in a real browser. After installing these tools, you can write test cases in separate test files and run them using Karma.

How do I test AngularJS services?

AngularJS services can be tested by injecting them into a test and mocking their dependencies. Jasmine provides a function called ‘spyOn’ that allows you to create a mock function and track its calls. You can use this function to mock the dependencies of the service and isolate it for testing. After setting up the mock, you can call the service’s methods and use Jasmine’s ‘expect’ function to verify their output.

How do I test AngularJS controllers?

Testing AngularJS controllers involves creating an instance of the controller and testing its methods and properties. You can create an instance of the controller using the $controller service provided by AngularJS. After creating the instance, you can call its methods and check their effects on the controller’s scope. You can also test the controller’s interaction with services by mocking the services and verifying the calls to their methods.

How do I test AngularJS providers?

Testing AngularJS providers is similar to testing services. You can inject the provider into a test, mock its dependencies, and test its methods. However, providers have a special method called ‘$get’ that returns the instance of the service. This method can be tested separately by calling it and checking the returned value.

How can I use dependency injection in AngularJS testing?

Dependency injection is a key feature of AngularJS that allows you to inject dependencies into components. In testing, you can use this feature to inject mock versions of dependencies into the component under test. This allows you to isolate the component and test it independently of its dependencies.

What are some common challenges in unit testing AngularJS components?

Some common challenges in unit testing AngularJS components include handling asynchronous operations, testing directives, and dealing with external dependencies. Asynchronous operations can make tests complex and hard to manage. Testing directives can be difficult due to their interaction with the DOM. External dependencies, such as services and APIs, can make tests unpredictable and hard to isolate.

How can I handle asynchronous operations in AngularJS tests?

Asynchronous operations in AngularJS tests can be handled using the $q service and the ‘done’ function provided by Jasmine. The $q service allows you to create promises that can be resolved or rejected in the test. The ‘done’ function can be called to signal that the asynchronous operation has completed.

How can I test AngularJS directives?

Testing AngularJS directives involves creating an instance of the directive and testing its behavior. You can create an instance of the directive using the $compile service provided by AngularJS. After creating the instance, you can manipulate it using jQuery-like methods and check its effects on the scope.

How can I deal with external dependencies in AngularJS tests?

External dependencies in AngularJS tests can be dealt with by mocking them. You can create a mock version of the dependency and inject it into the component under test. This allows you to control the behavior of the dependency and isolate the component for testing.