Unit Testing with AngularJS: Using the Mock Objects

Now that I have shown you how to test a simple controller, I am going to demonstrate how the different mock objects that I listed in Table 25-5 work.

1. 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. In Listing 25-6, you can see that I have updated the app.js file so that the controller it contains makes an Ajax request.

Listing 25-6. Adding an Ajax Request to the app.js File

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $http) {

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

});

$scope.counter = 0;

$scope.incrementCounter = function() {

$scope.counter++;

}

});

The controller requests the productData.json URL and uses a success function to receive the response and assign the data to a scope property called products. To test this new feature, I have extended the tests/controllerTest.js file, as shown in Listing 25-7.

Listing 25-7. Extending the Tests in the controllerTest.js File

describe(“Controller Test”, function () {

// Arrange

var mockScope, controller, backend;

beforeEach(angular.mock.module(“exampleApp”));

beforeEach(angular.mock.inject(function ($httpBackend) {

backend = $httpBackend;

backend.expect(“GET”, “productData.json”).respond(

[{ “name”: “Apples”, “category”: “Fruit”, “price”: 1.20 },

{ “name”: “Bananas”, “category”: “Fruit”, “price”: 2.42 },

{ “name”: “Pears”, “category”: “Fruit”, “price”: 2.02 }]);

}));

beforeEach(angular.mock.inject(function ($controller, $rootScope, $http) {

mockScope = $rootScope.$new();

$controller(“defaultCtrl”, {

$scope: mockScope,

$http: $http

});

backend.flush();

}));

// Act and Assess

it(“Creates variable”, function () {

expect(mockScope.counter).toEqual(0);

})

it(“Increments counter”, function () {

mockScope.incrementCounter();

expect(mockScope.counter).toEqual(1);

});

it(“Makes an Ajax request”, function () {

backend.verifyNoOutstandingExpectation();

});

it(“Processes the data”, function () {

expect(mockScope.products).toBeDefined();

expect(mockScope.products.length).toEqual(3);

});

it(“Preserves the data order”, function () {

expect(mockScope.products[0].name).toEqual(“Apples”);

expect(mockScope.products[l].name).toEqual(“Bananas”);

expect(mockScope.products[2].name).toEqual(“Pears”);

});

});

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 listed in Table 25-8.

Tip I have included the respond method in the table for ease of reference, but it is actually applied to the result from the expect method.

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.
  2. Send the responses.
  3. Check that all of the expected requests were made.
  4. Evaluate the results.

I describe each step in the sections that follow.

1.1. Defining the Expected Requests and Responses

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:

beforeEach(angular.mock.inject(function ($httpBackend) {

backend = $httpBackend;

backend.expect(“GET”, “productData.json”).respond(

[{ “name”: “Apples”, “category”: “Fruit”, “price”: 1.20},

{ “name”: “Bananas”, “category”: “Fruit”, “price”: 2.42},

{ “name”: “Pears”, “category”: “Fruit”, “price”: 2.02}]);

}));

In the example unit test, I used the inject method to obtain the $httpBackend service in order to call the expect method. I don’t have to take any special steps to get the mock object because the contents of the ngMocks module override the default service implementations.

Tip Just to be clear, the expect method defined by the mock $httpBackend service is entirely unrelated to the one defined that Jasmine uses to evaluate test results.

I told $httpBackend to expect a request made using the HTTP GET method for the productData.json URL, matching the request that the controller in the app.js file makes.

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 chapters. Notice that I don’t have to encode the data as JSON. This is done for me automatically.

1.2. Sending 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, as follows:

beforeEach(angular.mock.inject(function ($controller, $rootScope, $http) {

mockScope = $rootScope.$new();

$controller(“defaultCtrl”, {

$scope: mockScope,

$http: $http

});

backend.flush();

}));

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.

1.3. Checking That the Expected Requests Were Received

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 function, like this:

it(“Makes an Ajax request”, function () {

backend.verifyNoOutstandingExpectation();

});

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.

1.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, as follows:

it(“Processes the data”, function () {

expect(mockScope.products).toBeDefined();

expect(mockScope.products.length).toEqual(3);

});

it(“Preserves the data order”, function () {

expect(mockScope.products[0].name).toEqual(“Apples”);

expect(mockScope.products[l].name).toEqual(“Bananas”);

expect(mockScope.products[2].name).toEqual(“Pears”);

});

These are simple tests to ensure that the controller doesn’t mangle or rearrange the data, although in a real project the emphasis of HTTP testing is generally focused on the requests rather than the data handling.

2. Mocking Periods of Time

The mock $interval and $timeout services define extra methods that allow you to explicitly trigger the callback functions registered by the code being tested. In Listing 25-8, you can see how I have used the real services in the app.js file.

Listing 25-8. Adding Intervals and Timeouts to the app.js File

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $http, $interval, $timeout) {

$scope.intervalCounter = 0;

$scope.timerCounter = 0;

$interval(function () {

$scope.intervalCounter++;

}, 5000, 10);

$timeout(function () {

$scope.timerCounter++;

}, 5000);

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

});

$scope.counter = 0;

$scope.incrementCounter = function() {

$scope.counter++;

}

});

I have defined two variables, intervalCounter and timerCounter, that are incremented by functions passed to the $interval and $timeout services. These functions are called after five-second delays, which isn’t ideal in unit testing when the idea is to run a lot of tests quickly and often. Table 25-9 shows the additional methods defined by the mock versions of these services.

Listing 25-9. Adding Tests to the controllerTest.js File

describe(“Controller Test”, function () {

// Arrange

var mockScope, controller, backend, mocklnterval, mockTimeout;

beforeEach(angular.mock.module(“exampleApp”));

beforeEach(angular.mock.inject(function ($httpBackend) {

backend = $httpBackend;

backend.expect(“GET”, “productData.json”).respond(

[{ “name”: “Apples”, “category”: “Fruit”, “price”: 1.20 },

{ “name”: “Bananas”, “category”: “Fruit”, “price”: 2.42 },

{ “name”: “Pears”, “category”: “Fruit”, “price”: 2.02 }]);

}));

beforeEach(angular.mock.inject(function ($controller, $rootScope,

$http, $interval, $timeout) {

mockScope = $rootScope.$new();

mockInterval = $interval;

mockTimeout = $timeout;

$controller(“defaultCtrl”, {

$scope: mockScope,

$http: $http,

$interval: mockInterval,

$timeout: mockTimeout

});

backend.flush();

}));

// Act and Assess

it(“Creates variable”, function () {

expect(mockScope.counter).toEqual(0);

})

it(“Increments counter”, function () {

mockScope.incrementCounter();

expect(mockScope.counter).toEqual(1);

});

it(“Makes an Ajax request”, function () {

backend.verifyNoOutstandingExpectation();

});

it(“Processes the data”, function () {

expect(mockScope.products).toBeDefined();

expect(mockScope.products.length).toEqual(3);

});

it(“Preserves the data order”, function () {

expect(mockScope.products[0].name).toEqual(“Apples”);

expect(mockScope.products[1].name).toEqual(“Bananas”);

expect(mockScope.products[2].name).toEqual(“Pears”);

});

it(“Limits interval to 10 updates”, function () {

for (var i = 0; i < 11; i++) {

mocklnterval.flush(5000);

}

expect(mockScope.intervalCounter).toEqual(10);

});

it(“Increments timer counter”, function () {

mockTimeout.flush(5000);

expect(mockScope.timerCounter).toEqual(1);

});

});

3. Testing Logging

The mock $log service keeps track of the log messages it receives and presents them through a logs property that is added to the real service method names: log.logs, debug.logs, warn.logs, and so on. These properties make it possible to test that a unit code is logging messages correctly. In Listing 25-10, you can see that I have added the $log service to the app.js file.

Listing 25-10. Adding Logging to the app.js File

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope, $http, $interval, $timeout, $log) {

$scope.intervalCounter = 0;

$scope.timerCounter = 0;

$interval(function () {

$scope.intervalCounter++;

}, 5, 10);

$timeout(function () {

$scope.timerCounter++;

}, 5);

$http.get(“productData.json”).success(function (data) {

$scope.products = data;

$log.log(“There are ” + data.length + ” items”);

});

$scope.counter = 0;

$scope.incrementCounter = function() {

$scope.counter++;

}

});

I log a message each time the callback function registered with the $interval service is invoked. In Listing 25-11, you can see how I have used the mock $log service to make sure that the right number of logging messages have been written out.

Listing 25-11. Using the Mock $log Service in the controllerTest.js File

describe(“Controller Test”, function () {

// Arrange

var mockScope, controller, backend, mocklnterval, mockTimeout, mockLog;

beforeEach(angular.mock.module(“exampleApp”));

beforeEach(angular.mock.inject(function ($httpBackend) {

backend = $httpBackend;

backend.expect(“GET”, “productData.json”).respond(

[{ “name”: “Apples”, “category”: “Fruit”, “price”: 1.20 },

{ “name”: “Bananas”, “category”: “Fruit”, “price”: 2.42 },

{ “name”: “Pears”, “category”: “Fruit”, “price”: 2.02 }]);

}));

beforeEach(angular.mock.inject(function ($controller, $rootScope,

$http, $interval, $timeout, $log) {

mockScope = $rootScope.$new();

mockInterval = $interval;

mockTimeout = $timeout;

mockLog = $log;

$controller(“defaultCtrl”, {

$scope: mockScope,

$http: $http,

$interval: mockInterval,

$timeout: mockTimeout,

$log: mockLog

});

backend.flush();

}));

// Act and Assess

it(“Creates variable”, function () {

expect(mockScope.counter).toEqual(0);

})

it(“Increments counter”, function () {

mockScope.incrementCounter();

expect(mockScope.counter).toEqual(l);

});

it(“Makes an Ajax request”, function () {

backend.verifyNoOutstandingExpectation();

});

it(“Processes the data”, function () {

expect(mockScope.products).toBeDefined();

expect(mockScope.products.length).toEqual(3);

});

it(“Preserves the data order”, function () {

expect(mockScope.products[0].name).toEqual(“Apples”);

expect(mockScope.products[1].name).toEqual(“Bananas”);

expect(mockScope.products[2].name).toEqual(“Pears”);

});

it(“Limits interval to 10 updates”, function () {

for (var i = 0; i < 11; i++) {

mockInterval.flush(5000);

}

expect(mockScope.intervalCounter).toEqual(10);

});

it(“Increments timer counter”, function () {

mockTimeout.flush(5000);

expect(mockScope.timerCounter).toEqual(1);

});

it(“Writes log messages”, function () {

expect(mockLog.log.logs.length).toEqual(l);

});

});

The controller factory function writes a message to the $log.log method when it receives a response to its Ajax request. In the unit test, I read the length of the $log.log.logs array, which is where the messages written to the $log.log method are stored. In addition to the logs properties, the mock $log service defines the methods described in Table 25-10.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *