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

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

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

on
on javascript, angularjs, testing, ngmock

This is the second post following on from How to Unit Test $http in AngularJS - Part 1. This post covers the remaining arguments of when and expect, the respond function, and takes a deeper dive into the source code of ngMock’s $httpBackend.

Using the when and expect API calls

In the previous post we looked at the arguments method and url, in this post we will look at the data and headers arguments and how we can use them in our tests.

To recap, the parameters we can pass are:

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

And for expect (they are the same):

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

NB the [brackets] indicate ‘optional’.

As before, the arguments for when and expect are the same, so the following examples shown using when are also applicable for expect.

Using the data parameter

This is an optional parameter that we use to verify data being sent to an http function is what we expect. This would be used for POST and PUT.

The data argument can be of type:

  • Object
  • string
  • RegExp
  • function(string)

Object data Example

In this example we pass the data as a standard JavaScript object. The Object must exactly match the data being sent in the code under test:

  it('should post data (object)', inject(function($http) {
    
    var $scope = {};

    /* Code Under Test */
    $http.post('http://localhost/auth', { 
        username: 'hardcoded_user',
        password: 'hardcoded_password'
      })
      .success(function(data, status, headers, config) {
        $scope.user = data;
      });
    /* End Code */


    $httpBackend
      .when('POST', 'http://localhost/auth', { 
        username: 'hardcoded_user',
        password: 'hardcoded_password' 
      })
      .respond({ 
        username: 'hardcoded_user'
      });


    $httpBackend.flush();

    expect($scope.user).toEqual({ username: 'hardcoded_user' });

  }));

string data Example

In this example we pass the data as a string. The string format will be equivalent to calling stringify on a JSON object:

it('should post data (string)', inject(function($http) {
  
  var $scope = {};

  /* Code Under Test */
  $http.post('http://localhost/auth', { 
      username: 'hardcoded_user',
      password: 'hardcoded_password'
    })
    .success(function(data, status, headers, config) {
      $scope.user = data;
    });
  /* End Code */


  $httpBackend
    .when('POST', 'http://localhost/auth', '{"username":"hardcoded_user","password":"hardcoded_password"}')
    .respond({ 
      username: 'hardcoded_user'
    });


  $httpBackend.flush();

  expect($scope.user).toEqual({ username: 'hardcoded_user' });

}));

RegExp data Example

Here we use regex to match the data. This example regex allows for any user name value that has the password value ‘hardcoded_password’:

it('should post data (regex)', inject(function($http) {
  
  var $scope = {};

  /* Code Under Test */
  $http.post('http://localhost/auth', { 
      username: 'hardcoded_user',
      password: 'hardcoded_password'
    })
    .success(function(data, status, headers, config) {
      $scope.user = data;
    });
  /* End Code */


  $httpBackend
    .when('POST', 'http://localhost/auth', /{"username":".*","password":"hardcoded_password"}/)
    .respond({ 
      username: 'hardcoded_user'
    });

  $httpBackend.flush();

  expect($scope.user).toEqual({ username: 'hardcoded_user' });

}));

Function data Example

In this exampe we use a function that returns a boolean to verify that we have a match. The function in this example returns true for all cases where the username value is ‘hardcoded_user’:

it('should post data (function)', inject(function($http) {
  
  var $scope = {};

  /* Code Under Test */
  $http.post('http://localhost/auth', { 
      username: 'hardcoded_user',
      password: 'hardcoded_password'
    })
    .success(function(data, status, headers, config) {
      $scope.user = data;
    });
  /* End Code */


  $httpBackend
    .when('POST', 'http://localhost/auth', function(data) {
      return angular.fromJson(data).username === 'hardcoded_user'
    })
    .respond({ 
      username: 'hardcoded_user'
    });

  $httpBackend.flush();

  expect($scope.user).toEqual({ username: 'hardcoded_user' });

}));

Using the headers parameter

This is an optional parameter that we use to verify the headers being sent to an http function are what we expect. This would be useful in checking auth tokens on request headers, for example.

The headers argument can be of type:

  • Object
  • function(Object)

Object headers Example

In this example, we verify that the header property authToken has the value ‘teddybear’. Because the object has to match exactly, we also have to include the Accept property in the header:

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

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

  $httpBackend
    .when('GET', 'http://localhost/foo', undefined, {
      authToken: "teddybear", 
      Accept: "application/json, text/plain, */*"
    })
    .respond(200, { data: 'value' });

  $httpBackend.flush();

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

}));

Function headers Example

Note in the last example we had to also include the Accept propety to match the headers exactly. To match only on specific properties we could make use of the function argument as follows. In this example we can match just the authToken property from the headers of the code under test. The header object is passed to our callback function as an argument:

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

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

  $httpBackend
    .when('GET', 'http://localhost/foo', undefined, function(headers) {
      return headers.authToken === 'teddybear';
    })
    .respond(200, { data: 'value' });

  $httpBackend.flush();

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

}));

Using the respond function

Calling when or expect returns an object that has a respond function that we can use to configure the arguments that are passed to the http call under test once the http call is resolved (or in our case, when we call flush).

There are two methods of setting what properties are to be sent to an http request once it is fulfilled. We can pass the arguments directly or via a single function. The function signature is as follows:

function([status,] data[, headers, statusText]) 

OR

function(function(method, url, data, headers))

The arguments are:

  • Status - the http status code i.e. 200, 404, 500, 401 etc.
  • Data - this would be the data returned from an HTTP request e.g. JSON data
  • Headers - the HTTP headers that contains content type, auth details etc.
  • statusText - at the time of writing, this doesn’t appear to work.

#### Passing individual arguments

The fist method of setting response parameters is via function arguments passed to respond:

function([status,] data[, headers, statusText])

In this example we see how a success 200 status is returned, along with response data and headers for the matching URL based on the when configuration:

it('should use respond with params', inject(function($http) {
  
  var $scope = {};

  /* code under test */
  $http
    .get('http://localhost/foo')
    .success(function(data, status, headers, config, statusText) {
      // demonstrates how the values set in respond are used
      if (status === 200) {
        $scope.fooData = data;
        $scope.tokenValue = headers('token');
      }
    });
  /* end */

  $httpBackend
    .when('GET', 'http://localhost/foo')
    .respond(200, { data: 'value' }, { token: 'token value' }, 'OK');

  $httpBackend.flush();

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

}));

NB the values passed to respond are sent to the success function when the http request is resolved.

#### Passing a single function argument

The second method of setting the response values is via a function:

function(function(method, url, data, headers)

In this example, we create a generic when object that matches any URL, but make use of the arguments of the HTTP call passed to the function callback to verify its properties e.g. URL being called, http verb:

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

  /* Code Under Test */
  $http
    .get('http://localhost/foo')
    .success(function(data, status, headers, config, statusText) {
      // demonstrates how the values set in respond are used
      if (status === 200) {
        $scope.fooData = data;
        $scope.tokenValue = headers('token');
      }
    });
  /* End */

  $httpBackend
    .when('GET', /.*/)
    .respond(function(method, url, data, headers) {

      // example of how one might use function arguments to access request params
      if (url === 'http://localhost/foo') {
        return [200, { data: 'value' }, { token: 'token value' }, 'OK'];  
      }

      return [404];
    });

  $httpBackend.flush();

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

}));

NB note that our function returns an array to represent the values status, data, headers, statusText.

Re-using a respond function

When the respond function is called its return value is the same object, which means that we can re-configure respond on an existing when or expect object:

it('should re-use respond function', inject(function($http) {
  
  var $scope = {};

  /* code under test */
  var foo = function() {
    $http
      .get('http://localhost/foo')
      .success(function(data, status, headers, config, statusText) {
        // demonstrates how the values set in respond are used
        if (status === 200) {
          $scope.fooData = data;
          $scope.tokenValue = headers('token');
        }
      });
  };

  foo();
  foo();
  /* end */

  var whenObj = $httpBackend
    .when('GET', 'http://localhost/foo')
    .respond(200, { data: 'value' }, { token: 'token value' }, 'OK');

  $httpBackend.flush(1);

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

  whenObj.respond(200, { data: 'second value' }, { token: 'token value' }, 'OK');

  $httpBackend.flush(1);

  expect($scope.fooData).toEqual({ data: 'second value' });
  expect($scope.tokenValue).toEqual('token value');

}));

Flush

We’ve already seen extensive use of flush, which resolves all the pending HTTP requests that are matched against the test configuration. Using flush gives us synchronous control over the asynchronous HTTP calls in the code under test.

We can optionally pass a numeric argument to flush allowing us to resolve only a specific number of HTTP calls in sequential order. We demonstrated this in the previous code example. The following is code is the relevant snippet that uses flush([count]):

var whenObj = $httpBackend
  .when('GET', 'http://localhost/foo')
  .respond(200, { data: 'value' }, { token: 'token value' }, 'OK');

// Resolve one HTTP request (starting from the first registered)
$httpBackend.flush(1);

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

whenObj.respond(200, { data: 'second value' }, { token: 'token value' }, 'OK');

// Resolve one more HTTP request (which would be the last of the two HTTP calls in the code under test)
$httpBackend.flush(1);

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

Using the short methods

The when and expect functions are overloaded giving us shortcut access to the same underlying API. For example, for GET requests, we wouldn’t use the data argument, so a shorter formed API is available:

whenGET(url, [headers]);

There is the same for expectGET, whenPOST etc. See the ngMock $httpBackend documentation for the full list of overloaded methods. Now that you understand the underlying methods it will be easy to follow.

Additional Features

The remaining functions of the $httpBackend API give us additional ways of verifying that our code is handling HTTP requests as we expect:

  • verifyNoOutstandingExpectation(); - this is also called internally once flush is called and will throw an exception if there are any expect calls that have not been matched.

  • verifyNoOutstandingRequest(); - verifies that there are no outstanding requests that need to be flushed. We would likely use this when calling flush with an argument.

Finally, the following function allows us to re-set test expectations within the same test:

  • resetExpectations();

The following tests demonstrate these functions:

describe('using verify and reset', function() {

  it('should demonstrate usage of verifyNoOutstandingExpectation and reset', inject(function($http) {

    $httpBackend.expectGET('http://localhost/foo').respond(200);
    $httpBackend.expectGET('http://localhost/bar').respond(500);
    
    // without this, verifyNoOutstandingExpectation would throw an exception
    $httpBackend.resetExpectations();

    expect($httpBackend.verifyNoOutstandingExpectation).not.toThrow();
    expect($httpBackend.verifyNoOutstandingRequest).not.toThrow();

  }));

  it('should demonstrate usage of verifyNoOutstandingRequest', inject(function($http) {

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

    $http.get('http://localhost/foo');
    $http.get('http://localhost/foo');

    // NB we have only flushed the first http call, leaving one un-flushed
    $httpBackend.flush(1);
    
    expect($httpBackend.verifyNoOutstandingRequest).toThrow();

  }));

});

Example Test Code

Full code example of the tests used in this post via a Github Gist.

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