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
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 itsrespond
function to configure what the return properties should be e.g. status code, data response. -
When we call
flush()
, all thewhen
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 theexpect
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 thewhen
object. It’s mandatory forwhen
and optional forexpect
to have this. -
For the http calls in the code under test, there must be a matching
respond
function on either awhen
orexpect
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
.