Bradley Braithwaite
1 import {
2 Learning,
3 JavaScript,
4 AngularJS
5 } from 'bradoncode.com'
6 |

How to Unit Test $http in AngularJS - Part 1 - ngMock Fundamentals

How to unit test AngularJS code that uses the $http service with ngMock. Part 1 of 2.

on
on javascript, angularjs, testing, ngmock

In this post we delve into unit testing angular components that use the $http service. The http service is a core Angular service that allows us to make HTTP requests from our angular code. Under the covers it uses XMLHttpRequest, which is what we would use if using vanilla JavaScript. As with other native browser functions such as setTimeout, setInterval etc, angular has wrappers to facilitate unit testing. For our tests we will be making use of ngMock’s $httpBackend service.

This is a larger topic that will be spread across 2 posts. This is the first of the two where we will discuss the service, why it’s needed and see its basic usage for unit tests.

Update: Part 2 is now available.

Unit Testing $HTTP Service

Consider we had code that used angular’s $http service that we wanted to test. The following code makes an HTTP GET request to http://localhost/foo and if successful, sets properties on an object called $scope to indicate that the request was successful and to store the data response from the request:

$http.get('http://localhost/foo')
  .success(function(data, status, headers, config) {
    $scope.valid = true;
    $scope.response = data;
  })
  .error(function(data, status, headers, config) {
    $scope.valid = false;
});

In the following test, we make use of ngMock’s $httpBackend.when function. Using when allows us to configure a fake response to a particular http request based on its parameters (we will see more about this later).

Here’s the code for the test:

it('should demonstrate using when (200 status)', inject(function($http) {
  
  var $scope = {};

  /* Code Under Test */
  $http.get('http://localhost/foo')
    .success(function(data, status, headers, config) {
      $scope.valid = true;
      $scope.response = data;
    })
    .error(function(data, status, headers, config) {
      $scope.valid = false;
  });
  /* End */

  $httpBackend
    .when('GET', 'http://localhost/foo')
    .respond(200, { foo: 'bar' });

  $httpBackend.flush();

  expect($scope.valid).toBe(true);
  expect($scope.response).toEqual({ foo: 'bar' });

}));

Key Point:

  • We assert the status of the scope object to verify the HTTP behaviour performed as expected (by setting the properties on the $scope object).

  • The arguments passed to when are used to match the arguments used by the $http service in the code under test e.g. for a GET request to http://localhost/foo, return our hard-coded data.

  • We setup the $httpBackend.when object by calling its respond function to configure what the return properties should be e.g. status code, data response.

  • When we call flush(), all the when configurations are resolved giving us synchronous control over the asynchronous $http.get function in the code under test.

ngMock’s $httpBackend also has an expect function that accepts the same arguments as when. Here’s the same test as before, but using expect in place of when:

it('should demonstrate using expect (200 status)', inject(function($http) {
  
  var $scope = {};

  /* Code Under Test */
  $http.get('http://localhost/foo')
    .success(function(data, status, headers, config) {
      $scope.valid = true;
      $scope.response = data;
    }).error(function(data, status, headers, config) {
      $scope.valid = false;
  });
  /* End */

  $httpBackend
    .expect('GET', 'http://localhost/foo')
    .respond(200, { foo: 'bar' });

  expect($httpBackend.flush).not.toThrow();

  // NB we could still test the scope object properties as we did before...
  // expect($scope.valid).toBe(true);
  // expect($scope.response).toEqual({ foo: 'bar' });

}));

Key Point:

  • This is slightly different to the first test, since expect is more suited to throwing exceptions if specific conditions of the sequence of http calls are not met.

  • The flush function will throw an exception if the specific configuration of any of the expect configurations are not met.

We could also use a of mix the two:

it('should demonstrate using when and expect (200 status)', inject(function($http) {
  
  var $scope = {};

  /* code under test */
  $http.get('http://localhost/foo')
    .success(function(data, status, headers, config) {
      $scope.valid = true;
      $scope.response = data;
    }).error(function(data, status, headers, config) {
      $scope.valid = false;
  });
  /* end */

  $httpBackend
    .when('GET', 'http://localhost/foo')
    .respond(200, { foo: 'bar' });
  
  $httpBackend
    .expect('GET', 'http://localhost/foo');

  expect($httpBackend.flush).not.toThrow();
  expect($scope.valid).toBe(true);
  expect($scope.response).toEqual({ foo: 'bar' });

}));

Key Point:

  • Note that in this example the respond function is defined on the when object. It’s mandatory for when and optional for expect to have this.

  • For the http calls in the code under test, there must be a matching respond function on either a when or expect configuration or the test will fail.

when vs. expect? What’s the Difference

The when and expect APIs are similar, so why are both available? And when would we choose one over the other?

An example using when

In most cases we would use when, because most of the tests we write will entail some form of processing the response from the http request, that we could assert with a Jasmine expectation e.g. mapping properties of JSON data to objects. We have seen this in the test examples thus far.

As a rule of thumb: when allows us to setup re-usable fake responses for http calls.

An example using expect

We would use expect when we want to strictly test the http calls. By ‘strict’ we mean:

  • The exact sequence
  • The number of specific requests

Here’s a trivial example that encapsulates these points. Consider how we could unit test the following code. Note that we must verify the exact sequence and number of calls to a particular url, i.e. that we call the URLs ending 1, 2, 3 in that order, and that the url ending in 2 is called twice consecutively:

$http.get('http://localhost/1');
$http.get('http://localhost/2');
$http.get('http://localhost/2');
$http.get('http://localhost/3');

Here’s the test code:

it('should demonstrate using expect in sequence', inject(function($http) {
  
  $httpBackend.expect('GET', 'http://localhost/1').respond(200);
  $httpBackend.expect('GET', 'http://localhost/2').respond(200);
  $httpBackend.expect('GET', 'http://localhost/2').respond(200);
  $httpBackend.expect('GET', 'http://localhost/3').respond(200);
  
  /* Code under test */
  $http.get('http://localhost/1');
  $http.get('http://localhost/2');
  $http.get('http://localhost/2');
  $http.get('http://localhost/3');
  /* End */

  expect($httpBackend.flush).not.toThrow();

}));

Key Point:

  • As soon as well call $httpBackend.flush(); the expectations we setup are verified in sequence. If the exact sequence (from top to bottom) of the expect setup is not met, the test will fail with an error along the lines of Error: Unexpected request: GET http://localhost/3.

  • Once each expectation is met, it is removed from the configured list of expectations.

  • We couldn’t achieve the same result using when because the order and frequency of calls is not taken into consideration for these configurations.

To further demonstrate the point, the following test would pass (but shouldn’t) if we were using when:

it('should demonstrate bad case for using when', inject(function($http) {
  
  $httpBackend.when('GET', 'http://localhost/1').respond(200);
  $httpBackend.when('GET', 'http://localhost/2').respond(200);
  $httpBackend.when('GET', 'http://localhost/3').respond(200);

  /* Code under test */
  $http.get('http://localhost/2');
  $http.get('http://localhost/1');
  $http.get('http://localhost/3');
  $http.get('http://localhost/2');
  /* End */

  expect($httpBackend.flush).not.toThrow();

}));

Key Point:

  • We noted before that when configurations are re-usable, which means we cannot verify a sequence of calls.

Using the when and expect API calls

In the next sections we will explore the API for when and expect. The parameters that we can pass are the same for both, and as we discussed in the last example the key difference between the two methods is how the configurations are matched when we call flush. Therefore we will use when in the following code examples, but it would be the same for expect.

The parameters we can pass are:

when(method, url, [data], [headers]);

And for expect:

expect(method, url, [data], [headers]);

NB the [brackets] indicate ‘optional’.

Using the method parameter

We’ve already seen this in action. It’s used to match the HTTP verb to a configuration in the code under test. For example:

$httpBackend.expect('GET', 'http://localhost/1').respond(200);

This is a string value and can be one of:

  • GET
  • POST
  • PUT
  • PATCH
  • JSONP

Using the url parameter

Reminder: the following examples will use when, but the same can be used for expect.

The url value can be a:

  • string
  • RegExp
  • function(url)

We’ve already seen an example of passing the url as a string such as ‘http://localhost/foo’. What follows are examples of the other two approaches:

RegExp Url Example

This would be useful if we wanted to setup only one when configuration that will match multiple http calls. The following RegEx will match on the same host url but with an ending of foo or bar:

it('should use regex for url', inject(function($http) {
  
  var $scope = {};

  /* code under test */
  $http.get('http://localhost/foo')
    .success(function(data, status, headers, config) {
      $scope.fooData = data;
    });

  $http.get('http://localhost/bar')
    .success(function(data, status, headers, config) {
      $scope.barData = data;
    });
  /* end */

  $httpBackend
    .when('GET', /^http:\/\/localhost\/(foo|bar)/)
    .respond(200, { data: 'value' });

  $httpBackend.flush();

  expect($scope.fooData).toEqual({ data: 'value' });
  expect($scope.barData).toEqual({ data: 'value' });

}));

Function Url Example

We can also make use of a function that could have some specific checking. The function must return a boolean value to indicate whether the http request being checked matches what we expect. In this example, we check that the url starts with http://localhost/. Note that the URL being requested is passed as an argument to the function callback:

it('should use function for url', inject(function($http) {
  
  var $scope = {};

  /* code under test */

  $http.get('http://localhost/foo')
    .success(function(data, status, headers, config) {
      $scope.fooData = data;
    });

  $http.get('http://localhost/bar')
    .success(function(data, status, headers, config) {
      $scope.barData = data;
    });

  /* end */

  $httpBackend
    .when('GET', function(url) {
      return url.indexOf('http://localhost/') !== -1;
    })
    .respond(200, { data: 'value' });
  

  $httpBackend.flush();

  expect($scope.fooData).toEqual({ data: 'value' });
  expect($scope.barData).toEqual({ data: 'value' });

}));

This concludes Part 1. In Part 2 we will examine the remaining parameters, the respond function, take a look at a full example using a demo application and finally take a deeper dive into the source code of ngMock’s $httpBackend.

SHARE
Don't miss out on the free technical content:

Subscribe to Updates

CONNECT WITH BRADLEY

Bradley Braithwaite Software Blog Bradley Braithwaite is a software engineer who works for the search engine duckduckgo.com. He is a published author at pluralsight.com. He writes about software development practices, JavaScript, AngularJS and Node.js via his website . Find out more about Brad. Find him via:
You might also like:
mean stack tutorial AngularJS Testing - Unit Testing Tutorials