Michael Cranston

Angular $httpBackend Testing Gotchas

Click for the code of this post in a Plnkr

Imagine you have an Angular service:

app.service('UserService', function($http) {
  return {
    getProfile: function() {
      return $http.get('profile.json');
    },

    getAccount: function() {
      return $http.get('account_details.json');
    }
  }
});

This is our main app config:

app.config(function($stateProvider) {

    $stateProvider
    .state('main', {
      url: '/',
      templateUrl: 'template.html'
    });
});

Now, you want to write a simple unit test that ensures both methods on your service actually make a GET request.

it('userService.getAccount should make a GET call', function() {
  userService.getAccount();
  httpBackend.expectGET('account_details.json').respond(200, 'a response');
  httpBackend.flush();
});

it('userService.getProfile should make a GET call', function() {
  userService.getProfile();
  httpBackend.expectGET('profile.json').respond(200, 'a response');
  httpBackend.flush();
});

This will fail. You will see the following:

My Service userService.getAccount should make a GET call.
Error: Unexpected request: GET template.html Expected GET account_details.json
Error: Unexpected request: GET template.html
Expected GET account_details.json

This confused me. Why was $httpBackend trying to fetch the template on our main route?

The answer lies in the Angular docs:

Request expectations provide a way to make assertions about requests made by the application and to define responses for those requests. The test will fail if the expected requests are not made or they are made in the wrong order.

“Wrong order”. Angular’s $httpBackend service expected template.html as the first $http request, yet the unit test expects the account_details.json request to be the first.

Are you kidding me?

So this means, we need to satisfy the GET for template.html before we run our tests.

beforeEach(inject(function(_$httpBackend_, UserService) {
  httpBackend = _$httpBackend_;
  httpBackend.whenGET(/.html*/).respond(200, '');
  httpBackend.flush();
}));

Here’s the problem with the approach above. What if we have a resolve on our main app route as well?

httpBackend.whenGET(/.html*/).respond(200, '');
httpBackend.whenGET('some_resolve_url').respond(200, '');
httpBackend.flush();

Do we really want to have this in every test file when we need to test $http?

My solution was to create a helpers.js file that lives in the testing folder. I create a global called utils:

var utils = {
  appInitResolve: function(httpBackend) {
    httpBackend.whenGET(/.html*/).respond(200, '');
    httpBackend.whenGET('some_resolve_url').respond(200, '');
    httpBackend.flush();
  }
};

Now in each beforeEach, I call utils.appInitResolve(httpBackend). Now if our resolve considerations change, we can update them in one file rather than crowd all the beforeEach blocks.

comments powered by Disqus