AngularJS From Beginning: Unit Test Of AngularJS Components - Part 14

I am here to continue the discussion around AngularJS. Today, we will discuss how to perform unit tests of angular js components other than controller using Jasmine. Also in case you have not had a look at our previous articles of this series, go through the following links:

In this article, we will discuss how to perform unit tests using $http or other angularjs service including angularjs directives. First we will discuss about the http responses.

Mocking HTTP Responses

The $httpBackend service provides a low-level API that is used by the $http service to make Ajax requests (and by the $resource service, which in turn relies on $http). The mock $httpBackend service included in the ngMocks module makes it easy to consistently simulate responses from a server, which allows a unit of code to be isolated from the vagaries of real servers and networks. The mock $httpBackend service provides an API that matches requests made through the $http service to canned results and to control when those canned results are sent. The methods defined by the mock $httpBackend service are as below.

NameDescriptions
expect(method, url, data, headers)Defines an expectation for a request that matches the method and URL(with optional data and header matches)
flush()

flush(count)
Sends back pending results (or the specified number of responses ifthe optional argument is used)
resetExpectations()Resets the set of expectations
verifyNoOutstandingExpectation()Checks that all of the expected requests have been received
respond(data)

response(status, data, headers)
Defines a response for an expected request

The process for using the mock $httpBackend service is relatively simple and has the following steps: 

  1. Define the requests that you expect to get and the responses for them

    The expect method is used to define a request that you expect the component being tested to make. The required arguments are the HTTP method and the URL that will be requested, but you can also provide data and headers that will be used to narrow the request that will be matched. The result from the expect method is an object on which the respond method can be called. I have used the basic form of this method, which takes a single argument for the data that will be returned to simulate a response from the server. I used some of the product data from earlier articles. Notice that I don’t have to encode the data as JSON. This is done for me automatically.

  2. Send the responses

    To reflect the asynchronous nature of Ajax requests, the mock $httpBackend service won’t send its canned responses until the flush method is called. This allows you to test the effect of long delays or timeouts, but for this test I want the response sent as soon as possible, so I call the flush method immediately after the controller factory function is executed. Calling the flush method resolves the promise returned by the $http service and executes the success function defined by the controller. Notice that I have to use the inject method to obtain the $http service so that I can pass it to the factory function through the $controller service.

  3. Check that all of the expected requests were made

    The $httpBackend service expects to receive one HTTP request for each use of the expect method, which makes it easy to check that the code being tested has made all of the requests you expect. My code makes only one request, but I still checked to see that all of my expectations have been met by calling the verifyNoOutstandingExpectation method within a Jasmine it functions. The verifyNoOutstandingExpectation method will throw an exception if not all of the expected requests have been received; for this reason, you don’t need to use the Jasmine expect method.

  4. Evaluate the results

    The final step is to evaluate the results of the test. Since I am testing a controller, I perform my tests on the scope object I created
app.js
  1. var testApp = angular.module('TestApp', []);  
 controller.js
  1. testApp.controller('httpController'function ($scope, $http) {  
  2.   
  3.     $http.get("productData.json").success(function (data) {  
  4.         $scope.products = data;  
  5.     });  
  6.     $scope.counter = 0;  
  7.     $scope.hitCounter = function () {  
  8.         $scope.counter++;  
  9.     }  
  10.   
  11. });  
Index.html
  1. <!doctype html>  
  2. <html>  
  3. <head>  
  4.     <title>Jasmine Spec Runner</title>  
  5.     <link href="../test_resource/jasmine.css" rel="stylesheet" />  
  6. </head>  
  7. <body>  
  8.     <script src="../test_resource/jasmine.js"></script>  
  9.     <script src="../test_resource/jasmine-html.js"></script>  
  10.     <script src="../test_resource/boot.js"></script>  
  11.   
  12.     <script src="../../../RefScript/angular.min.js"></script>  
  13.     <script src="../test_resource/angular-mocks.js"></script>  
  14.     <script src="../CodeJs/testApp.js"></script>  
  15.     <script src="../CodeJs/httpController.js"></script>  
  16.   
  17.     <script src="spec/test.js"></script>  
  18. </body>  
  19. </html>  
test.js
  1. (function () {  
  2.     'use strict';  
  3.   
  4.     describe('Controller Test'function () {  
  5.         debugger;  
  6.         var mockScope = {};  
  7.         var controller, backend;  
  8.   
  9.         beforeEach(angular.mock.module("TestApp"));  
  10.   
  11.         beforeEach(angular.mock.inject(function ($httpBackend) {  
  12.             backend = $httpBackend;  
  13.             backend.expect("GET""productData.json").respond(  
  14.                 [  
  15.                     { "name""Apples""category""Fruit""price": 1.20 },  
  16.                     { "name""Bananas""category""Fruit""price": 2.42 },  
  17.                     { "name""Pears""category""Fruit""price": 2.02 }  
  18.                 ]);  
  19.         }));  
  20.   
  21.         beforeEach(angular.mock.inject(function ($controller, $rootScope, $http) {  
  22.             mockScope = $rootScope.$new();  
  23.             $controller("httpController", {  
  24.                 $scope: mockScope,  
  25.                 $http: $http  
  26.             });  
  27.             backend.flush();  
  28.         }));  
  29.   
  30.         it("Creates variable"function () {  
  31.             expect(mockScope.counter).toEqual(0);  
  32.         });  
  33.   
  34.         it("Increments counter"function () {  
  35.             mockScope.hitCounter();  
  36.             expect(mockScope.counter).toEqual(1);  
  37.         });  
  38.         it("Makes an Ajax request"function () {  
  39.             backend.verifyNoOutstandingExpectation();  
  40.         });  
  41.         it("Processes the data"function () {  
  42.             expect(mockScope.products).toBeDefined();  
  43.             expect(mockScope.products.length).toEqual(3);  
  44.         });  
  45.         it("Preserves the data order"function () {  
  46.             expect(mockScope.products[0].name).toEqual("Apples");  
  47.             expect(mockScope.products[1].name).toEqual("Bananas");  
  48.             expect(mockScope.products[2].name).toEqual("Pears");  
  49.         });  
  50.     });  
  51. })();   

Test  a Custom Filter

For perform a unit test for a custom filter, we can obtain instances of a filter through $filter service, which we already discussed in the series earlier. In case of custom filter, we need to use inject method to obtain an instance of the $filter service and use it to obtain an instance of the filter, which we need to assign within a new variable. We need to obtain the filter object within a beforeEach function, which means that we always get a fresh instance of each test.

Testing a Directive

Testing a directive is a little more complicated because of the way that directives are applied to, and can modify, HTML. This means unit tests for directives rely on jqLite and the $compile service, use the inject method to obtain the $rootScope and $compile services. We need to create a new scope and assign the data that the directive will use to the data property. We also can keep a reference to the $compile service so that we can use it in the test. We need to compile a fragment of HTML to which the directive has been applied, specifying that the source of the data is the scope data array. This produces a function that we need to invoked with the mock scope to get the HTML output from the directive. To assess the results, we can use jqLite to check the structure and the order of the elements that the directive has produced.

Testing a Service

Obtaining an instance of a service to test is easy because the inject method can be used, just as I have been doing to get the built-in and mocked services in earlier tests. We used the factory method described in earlier article, to define a service that maintains a counter and that defines methods that increment and return the counter value. This isn’t a useful service in its own right, but it lets me demonstrate the process for testing a service.
 
Angular components js file
  1. testApp.controller('httpController'function ($scope, $http) {  
  2.   
  3.     $http.get("productData.json").success(function (data) {  
  4.         $scope.products = data;  
  5.     });  
  6.     $scope.counter = 0;  
  7.     $scope.hitCounter = function () {  
  8.         $scope.counter++;  
  9.     }  
  10.   
  11. });  
  12.   
  13. testApp.filter("labelCase"function () {  
  14.     return function (value, reverse) {  
  15.         if (angular.isString(value)) {  
  16.             var intermediate = reverse ? value.toUpperCase() : value.toLowerCase();  
  17.             return (reverse ? intermediate[0].toLowerCase() :  
  18.             intermediate[0].toUpperCase()) + intermediate.substr(1);  
  19.         } else {  
  20.             return value;  
  21.         }  
  22.     };  
  23. });  
  24.   
  25. testApp.directive("unorderedList"function () {  
  26.     return function (scope, element, attrs) {  
  27.         var data = scope[attrs["unorderedList"]];  
  28.         if (angular.isArray(data)) {  
  29.             var listElem = angular.element("<ul>");  
  30.             element.append(listElem);  
  31.             for (var i = 0; i < data.length; i++) {  
  32.                 listElem.append(angular.element('<li>').text(data[i].name));  
  33.             }  
  34.         }  
  35.     }  
  36. });  
  37.   
  38. testApp.factory("counterService"function () {  
  39.     var counter = 0;  
  40.     return {  
  41.         incrementCounter: function () {  
  42.             counter++;  
  43.         },  
  44.         getCounter: function () {  
  45.             return counter;  
  46.         }  
  47.     }  
  48. });  
Index.html
  1. <!doctype html>  
  2. <html>  
  3. <head>  
  4.     <title>Jasmine Spec Runner</title>  
  5.     <link href="../test_resource/jasmine.css" rel="stylesheet" />  
  6. </head>  
  7. <body>  
  8.     <script src="../test_resource/jasmine.js"></script>  
  9.     <script src="../test_resource/jasmine-html.js"></script>  
  10.     <script src="../test_resource/boot.js"></script>  
  11.   
  12.     <script src="../../../RefScript/angular.min.js"></script>  
  13.     <script src="../test_resource/angular-mocks.js"></script>  
  14.     <script src="../CodeJs/testApp.js"></script>  
  15.     <script src="../CodeJs/httpController.js"></script>  
  16.   
  17.     <script src="spec/test.js"></script>  
  18.     <script src="spec/test_directive.js"></script>  
  19.     <script src="spec/test_service.js"></script>  
  20. </body>  
  21. </html>  
test.js (for filter test)
  1. (function () {  
  2.     'use strict';  
  3.   
  4.     describe('Filter Test'function () {  
  5.         var filterInstance;  
  6.         beforeEach(angular.mock.module("TestApp"));  
  7.   
  8.         beforeEach(angular.mock.inject(function ($filter) {  
  9.             filterInstance = $filter("labelCase");  
  10.         }));  
  11.   
  12.         it("Changes case"function () {  
  13.             var result = filterInstance("test phrase");  
  14.             expect(result).toEqual("Test phrase");  
  15.         });  
  16.   
  17.         it("Reverse case"function () {  
  18.             var result = filterInstance("test phrase"true);  
  19.             expect(result).toEqual("tEST PHRASE");  
  20.         });  
  21.     });  
  22. })();  
Test_directive.js (for directive test)'
  1. describe("Directive Tests"function () {  
  2.     var mockScope;  
  3.     var compileService;  
  4.     beforeEach(angular.mock.module("TestApp"));  
  5.   
  6.     beforeEach(angular.mock.inject(function ($rootScope, $compile) {  
  7.         mockScope = $rootScope.$new();  
  8.         compileService = $compile;  
  9.         mockScope.data = [  
  10.                             { name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },  
  11.                             { name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },  
  12.                             { name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }  
  13.         ];  
  14.     }));  
  15.   
  16.     it("Generates list elements"function () {  
  17.         var compileFn = compileService("<div unordered-list='data'></div>");  
  18.   
  19.         var elem = compileFn(mockScope);  
  20.   
  21.         expect(elem.children("ul").length).toEqual(1);  
  22.         expect(elem.find("li").length).toEqual(3);  
  23.         expect(elem.find("li").eq(0).text()).toEqual("Apples");  
  24.         expect(elem.find("li").eq(1).text()).toEqual("Bananas");  
  25.         expect(elem.find("li").eq(2).text()).toEqual("Pears");  
  26.     });  
  27. });  
test_service.js (for service test)
  1. (function () {  
  2.     'use strict';  
  3.   
  4.     describe("Service Tests"function () {  
  5.   
  6.         beforeEach(angular.mock.module("TestApp"));  
  7.   
  8.         it("Increments the counter"function () {  
  9.             angular.mock.inject(function (counterService) {  
  10.                 expect(counterService.getCounter()).toEqual(0);  
  11.                 counterService.incrementCounter();  
  12.                 expect(counterService.getCounter()).toEqual(1);  
  13.             });  
  14.         });  
  15.     });  
  16. })();