doker doker - 6 months ago 18
AngularJS Question

How to specify http response order in angular js tests?

A controller makes 2 calls to a remote http location to get data.
When data comes a procedure is called. When both requests return data, then data merging is done and some aggregation is performed.

The purpose of a unit test would be to test if the controller works as expected no matter the order of responses.

it("downloads all data and combines it", function() {
...
$httpBackend.expectGET(responsePerDomainQuery).respond(
{ result: [ { result: 2 }, { result: 3 } ] });
$httpBackend.expectGET(responsePerTrQuery).respond(
{ result: [{ result: 1 }, { result: 4 }] });
$controller("Ctrl", { '$scope': $scope });
$httpBackend.flush();
... some expectations ...
}


The test passes but it does not guarantee that any order of successfully responding requests will not break the controller's logic. How can this be achieved?

Answer

When I said "no need to test this case" i was referring to the fact that using $q.all already guarantees that the callback is executed only when all of the requests are satisfied. That being said I agree that preparing tests for your own implementation is a good practice, so here's I would do it.

(Mind that this is just pseudo code, some things may need to be tweaked in order to work properly, but that's just to explain how i would tackle this one.)

First of all I would move my AJAX calls away from my controller and provide a dedicated service for them (maybe you already did it this way, if so that's great, bear with me for now).

As an example:

angular.service('myQueries', function($http){
   this.myReq1 = function(){
      return $http.get(API.url1);
   };
   this.myReq1 = function(){
      return $http.get(API.url2);
   };
});

Then I would test this service on its own normally using $httpBackend.expectGET().

I would then get back to the controller and use that service in there as specified in my comments to the question:

angular.controller('myCtrl', function($scope, myQueries, $q){
  // at load time query for results
  $q.all([myQueries.myReq1(), myQueries.myReq2()])
     // everything after this is guaranteed to be run ONLY when
     // both responses are in our hands
    .then(doSomethingWithBoth)
     // one or both requests went bad
     // let's handle this situation too.
    .catch(someThingWentBad);

  function doSomethingWithBoth(data){
    $scope.myData = data;
  }

  function someThingWentBad(data){
    $scope.disaster = true;
  }
});

At this point we can test our controller and inject a mocked service into it. Many ways to do it but something similar should do:

 var scope, controller, fakeService, q, dfd1, dfd2;

 beforeEach(function(){
  fakeService = {
    myReq1: function(){
      dfd1 = q.defer();
      return dfd1.promise;
    },
    myReq2: function(){
      dfd2 = q.defer();
      return dfd2.promise;
    },
   };
 })

beforeEach(inject(function ($rootScope, $controller, $q) {
    q = $q;
    scope = $rootScope.$new();
    controller = $controller('myCtrl', { $scope: scope, myQueries: fakeService });
}));

At this point you are free to resolve/reject the promises exactly when you want. You can check what happens when the first response is faster than the second:

it('should do this when one response is faster', function(){
  dfd1.resolve('blabla');
  // myReq2 is still pending so doSomethingWithBoth() has not yet been called
  scope.$apply();
  expect(scope.myData).toBe(undefined);
  dfd2.resolve('i am late, sorry');
  scope.$apply();
  expect(scope.myData).not.toBe(undefined);
});

You can check what happens when the second response is faster than the first:

it('should do this when the other response is faster', function(){
  dfd2.resolve('here is a response');
  // myReq1 is still pending so doSomethingWithBoth() has not yet been called
  scope.$apply();
  expect(scope.myData).toBe(undefined);
  dfd1.resolve('i am late, sorry');
  scope.$apply();
  expect(scope.myData).not.toBe(undefined);
});

Or what happens when one of those fails:

it('should do this when one response fails', function(){
  dfd1.resolve('blabla');
  dfd2.reject();
  scope.$apply();
  expect(scope.disaster).toBeTruthy();
});