Check out myAngular article series with live demos and Facebook group Angular - Advanced Topics

Comprehensive guide to Unit Testing in AngularJS

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Thu Aug 13 2015

Angular allows you to build amazing user interfaces, but as complexity increases, unit testing becomes a very important part of your project. In this article I will provide a comprehensive guide for how to successfully write unit tests in AngularJS. The examples are created using Jasmine, but the concepts are not directly tied to a specific testing framework.

Unit testing

Unit testing is an art, but the mastery of this art relies more on writing testable code than being a master at writing tests. Angular and its default MVC pattern aims to serve as a pattern for writing testable code. However, MVC alone is not a guaranteed path to testable code, but it does provide a skeleton, and introduces you to a key factor – separation of concerns. Separation of concerns is a guiding principle in Software Engineering where the general idea is to limit the functionality of a component to a well defined and specific task. Granted, it's up to the developer to define the scope of this responsibility, but the more responsibility you assign to a component, the harder it will be to unit test it.

Separating distinct features into independent components makes testing much easier, but it's often unrealistic for components to be free of integration with other components. Unit testing is generally a big driver behind abstractions in code, and when done correctly, you can take advantage of these abstractions for mocking. Simulating integration points through mocking is a great way to ensure that your tests only concern themselves with the direct responsibilities of the component under test.

Challenges

All this sounds simple in theory, but my experience is that it takes years to fully master the art of unit testing. Unit testing skills tend to evolve with system design skills as developers learn to embrace the ideas of single responsibility and abstractions. Developers who struggle with unit tests are often guilty of written their code without tests in mind, and are learning the hard way, that retrofitting tests is no easy task.

A common consequence of writing code without tests in mind is test code that is orders of magnitude more complicated than the code under test. This is clearly not a good position to be in since these complicated tests may, strictly speaking, require their own tests now.

Practical Examples

In the following sections I will illustrate how to write unit tests for some of the more common scenarios in AngularJS.

Simple Service

A service in its simplest form is some form of Angular entity without any external dependencies. In the following example I've created an add-service with a simple add method. This is a good place to start since it's an example of a component that can be tested in complete isolation without having to simulate any integration points.

angular.module('math',[]).factory('add-service',[ function(){ function add(o1,o2){ return o1 + o2; } return {add:add}; } ]); //Test fixture describe('add', function(){ var addService; beforeEach(function(){ module('math'); inject( function($injector){ addService = $injector.get('add-service'); }); }); it('should add two numbers', function(){ var result = addService.add(5,5); expect(result).toBe(10); }); });

There are a few stylistic variations when it comes to writing Jasmine tests, but the key players are a 'describe' to define the test fixture and an 'it' method per unit test. You may have multiple tests in one describe block. You may even have nested describe blocks if you prefer that as a strategy for grouping tests based on the feature under test.

Another common fixture in unit tests is the beforeEach function. This function executes before each test and is a good way to share common code. Pay extra attention to the inject method from Angular mock which enables us to inject named Angular dependencies the same way you would do it in regular Angular DI.

In the test itself you can see that I am calling the add method and asserting the result using the built in Jasmine expect function.

Simple service with dependency on another simple service

The next natural evolution of the previous example is to unit test a service with a dependency on another service. The code below shows a formatting service that expects a customer object to be returned from the external customer-service. At this point I want to add a test for the formatting logic, but ideally I would like to avoid depending on the actual customer-service implementation.

angular.module('customer').factory('customer-formatting-service',[ 'customer-service', function(customerService){ function getFormattedCustomerInfo(customerId){ var customer = customerService.getCustomerById(customerId); return customer.firstName + ' ' + customer.lastName + ' Total Sales: $' + customer.totalSales } return {getFormattedCustomerInfo:getFormattedCustomerInfo}; } ]); //Test fixture describe('getFormattedCustomerInfo', function(){ var customerService; var customerFormattingService; beforeEach(function(){ module('customer'); inject( function($injector){ customerService = $injector.get('customer-service'); customerFormattingService = $injector.get('customer-formatting-service'); spyOn(customerService,['getCustomerById']).and.returnValue({firstName:'Joe',lastName:'Smith',totalSales:50}); }); }); it('should return formatted customer information', function(){ var formatted = customerFormattingService.getFormattedCustomerInfo(1); expect(customerService.getCustomerById).toHaveBeenCalledWith(1); expect(formatted).toBe('Joe Smith Total Sales: $50'); }); });

The key difference from the previous example is that we have an added challenge of removing the dependency on an external service. Luckily, this is actually pretty straight forward.

In order to abstract the customer-service implementation I am creating a spy which effectively adds a mock method to the original customer-service. This means we are able to inject our mock behavior whenever getCustomerById is called.

Service with dependency on asynchronous service

The previous example introduced us to mocking, but in the following example I will take this a step further by showing how to mock asynchronous services. Knowing how to unit test asynchronous code is very important in Angular since asynchronous execution and promises are so fundamental to how Angular works.

In the code below I have modified the customer service slightly to return a promise instead of a customer object directly.

angular.module('customer').factory('customer-formatting-service-async',[ 'customer-service', function(customerService){ function getFormattedCustomerInfo(customerId){ return customerService.getCustomerById(customerId) .then(function(customer){ return customer.firstName + ' ' + customer.lastName + ' Total Sales: $' + customer.totalSales; }) .catch(function(e){ return {error:e}; }); } return {getFormattedCustomerInfo:getFormattedCustomerInfo}; } ]);

You might find this example to me more realistic since the majority of api calls in Angular will be asynchronous and expose you to promises.

The goal of our next test is to successfully test that the asynchronous call resolved and was dealt with correctly.

describe('getFormattedCustomerInfo-async', function(){ var customerService; var customerFormattingService; var $q; var $scope; beforeEach(function(){ module('customer'); inject( function($injector){ customerService = $injector.get('customer-service'); customerFormattingService = $injector.get('customer-formatting-service-async'); $q = $injector.get('$q'); $scope = $injector.get('$rootScope').$new(); }); }); it('should return formatted customer information based on async call', function(done){ spyOn(customerService,['getCustomerById']).and.returnValue($q.when({firstName:'Joe',lastName:'Smith',totalSales:50})); customerFormattingService.getFormattedCustomerInfo(1).then(function(formatted){ expect(customerService.getCustomerById).toHaveBeenCalledWith(1); expect(formatted).toBe('Joe Smith Total Sales: $50'); done(); }); $scope.$digest(); }); });

As you can tell, there are a number of new concepts to consider in the test to deal with the asynchronous aspect of the code under test. The first thing to notice is the mock. Instead of a plain customer object our mock now returns a promise generated by the $q service. You may also have noticed that the expect calls have been moved into a then block. This is so that we can execute the expects when the promise resolves.

Resolving the promise is key here, but there is some risk when you are trying to test asynchronous code when the unit test itself executes synchronously. If you're not careful you might end up with falsely passing tests because the asynchronous timing is not dealt with appropriately. A potential issue is that your test may call the asynchronous code, but completes before the assertions are executed, since the promise never resolved during the execution of the test.

Luckily there is a built in safety mechanism in the Jasmine done method. Using the overload of the it method that takes a done parameter will guarantee that the test won't exit until the done method has been called.

You might be wondering why we are calling $scope.$digest() at the end of the test. As you may know, $digest will trigger a digest cycle in Angular and we need the digest cycle to resolve the promise. It's a feature of the $q library that it only processes promises during a digest cycle and unit tests are no exception from this rule.

In our next test we will simulate an error condition in the asynchronous call and test if the error condition is dealt with correctly.

it('should handle error from async call', function(done){ spyOn(customerService,['getCustomerById']).and.returnValue($q.reject('api error')); customerFormattingService.getFormattedCustomerInfo(1).then(function(e){ expect(customerService.getCustomerById).toHaveBeenCalledWith(1); expect(e).toEqual({error:'api error'}); done(); }); $scope.$digest(); });

Conceptually this test is very similar to the previous one, but the instead of resolving the promise, our mock will now reject the promise. Rejecting the promise will trigger the catch block in the code under test and return an error message.

Mocking entire services

The previous examples showed us how to mock services using the spy method. This is a powerful technique, but requires us to mock piecemeal by spying on individual methods. What if we want to replace the entire service with an alternative implementation? It turns out that the $provide method will let us do just that.

describe('show how to use $provide', function(){ var customerFormattingService; var customerServiceMock = { getCustomerById:function(){return {firstName:'Joe',lastName:'Smith',totalSales:50}}, getCustomerByPhoneNumber:function(){}, getCustomerByName:function(){} }; beforeEach(module('customer', function($provide) { $provide.factory('customer-service', function(){return customerServiceMock;}); })); beforeEach(function(){ inject( function($injector){ customerFormattingService = $injector.get('customer-formatting-service'); }); }); it('should show how to define a mock using $provide', function(){ var formatted = customerFormattingService.getFormattedCustomerInfo(1); expect(formatted).toBe('Joe Smith Total Sales: $50'); }); });

In the example above you can see how to mock a service by replacing the entire implementation with an alternative implementation. This approach differs from the spy approach since we don't have to spy on individual methods. However it's worth pointing out that we no longer have call tracking to determine if a method was called or not.

Controllers

Testing controllers is actually easier than services because of the handy $controller function that lets us instantiate controllers and pass in any dependencies during declaration or runtime.

I have created a simple controller and will demonstrate how to write a simple test for it.

angular.module('customer').controller('customer-controller', [ 'customer-formatting-service', function(customerFormattingService){ this.productName = 'Food'; this.getSalesDate = function(){ return new Date(); }; this.printSalesReport = function(customerId){ var customerInfo = customerFormattingService.getFormattedCustomerInfo(customerId); return this.productName + ' ' + customerInfo + ' ' + this.getSalesDate().toDateString(); }; } ]);

Below is a test of the printSalesReport method.

describe('customer-controller', function(){ var $controller; var customerService; beforeEach(function(){ module('customer'); inject( function($injector){ customerService = $injector.get('customer-service'); $controller = $injector.get('$controller'); }); }); it('should print sales report', function(){ spyOn(customerService,'getCustomerById').and.returnValue({firstName:'Joe', lastName:'Smith',totalSales:50}); var ctrl = $controller('customer-controller'); //Override controller functions ctrl.productName = 'Snacks'; ctrl.getSalesDate = function(){return new Date(2000,1,1);}; var report = ctrl.printSalesReport(1); expect(report).toBe('Snacks Joe Smith Total Sales: $50 Tue Feb 01 2000'); }); });

As you can tell I am passing in the customerService dependency, but also overriding one of the controller methods to abstract the date dependency.

Directives

Directives serve as a great way to extend html by packaging functionality into custom components. Using directives is the recommended approach for anything DOM related, but for testability it's important to avoid mixing DOM manipulation with your logic. I generally recommend putting all JavaScript logic into an external controller that you reference from the directive. This makes it possible to test your logic in isolation from the directive – without a dependency on the DOM.

In the following example I have created a directive with a controllerAs style controller. As you can tell the logic is completely separated from the directive.

angular.module('customer').controller('customerGreetingController', [ 'customer-service', function(customerService) { var customer = customerService.getCustomerById(this.id); this.greeting = this.message + ' ' + customer.firstName + '!'; this.items = []; this.purchaseItem = function(item){ this.items.push(item); }; } ]); angular.module('customer').directive('customer-greeting', function(){ return{ scope:{}, restrict:'E', templateUrl:'greeting.html', bindToController:{message:'@',id:'@'}, controller:'customerGreetingController', controllerAs:'customerGreetingController' }; });

However to test the controller we have to simulate the directive in the test in order to pass the message and id properties to the controller. It turns out that Angular has added support for this via the bindToController property. BindToController effectively attaches these properties to the 'this' reference of the controller.

Next I will show how to take advantage of this in our unit test:

describe('CustomerDirective', function(){ var customerService; var $controller; beforeEach(function(){ module('customer'); inject( function($injector){ customerService = $injector.get('customer-service'); $controller = $injector.get('$controller'); spyOn(customerService,['getCustomerById']).and.returnValue({firstName:'Joe'}); }); }); it('should create greeting', function(){ var ctrl = $controller('customerGreetingController', {customerService:customerService},{message:'greetings'}); expect(ctrl.greeting).toBe('greetings Joe!'); }); it('should add purchased item', function(){ var ctrl = $controller('customerGreetingController', {customerService:customerService},{message:'greetings'}); ctrl.purchaseItem('Milk'); ctrl.purchaseItem('Bread'); expect(ctrl.items.length).toBe(2); expect(ctrl.items[0]).toBe('Milk'); expect(ctrl.items[1]).toBe('Bread'); }); });

It turns out passing the dependencies is easy because of an optional third parameter to $controller. This enables us to pass in properties to simulate values passed from the directive to the controller without even bringing the directive portion into the test.

If you liked this article, share it with your friends on social media:

We also have a new Facebook group about advanced Angular topics.

I invite you to follow me on twitter