Unit Testing in AngularJS: Services, Controllers & Providers

article

#1

Originally published at: http://www.sitepoint.com/unit-testing-angularjs-services-controllers-providers/

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.

Continue reading this article on SitePoint


#2

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!


#3

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.


#4

Should be fixed now.


#5

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


#7

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


#8

This helped me a lot. Thanks


#9

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.


#10

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.


#11

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


#12

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.