Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test an angularjs promise chain using $httpBackend

Using AngularJS, I am trying to unit test a function that makes multiple calls to $http.

My test looks something like this:

it('traverses over a hierarchical structure over multiple chained calls', function() {

    myService.traverseTheStuff()
    .then(function(theAggregateResult) {
        // ...is never fulfilled
    });

    $httpBackend.flush();
});

Other single-call tests will register the callback passed to .then() and execute it as soon as I call .flush().

The code under test looks something like this.

function traverseTheStuff(){

    // This will make a call to $http to fetch some data
    return getRootData()

    // It is fulfilled at the end of the test when I $httpBackend.flush()
    .then(function(rootData){

        // Another call to $http happens AFTER $httpBackend.flush()
        return getNextLevel(rootData.someReference);
    })

    // The second promise is never fulfilled and the test fails
    .then(function(nextLevel){
        return aggregateTheStuff(...);
    });
}

For what its worth, each of the single calls is unit tested separately. Here, I want to traverse a tree, aggregate some data and unit test a) that the promise chaining is hooked up correctly and b) the aggregation is accurate. Flattening it out into separate discrete calls is already done.

like image 853
Craig Celeste Avatar asked Dec 06 '13 05:12

Craig Celeste


People also ask

How to do unit testing in Angular js?

Testing in AngularJS is achieved by using the karma framework, a framework which has been developed by Google itself. The karma framework is installed using the node package manager. The key modules which are required to be installed for basic testing are karma, karma-chrome-launcher ,karma-jasmine, and karma-cli.

How to test Directive AngularJS?

If a directive creates its own scope and you want to test against it, you can get access to that directive's scope by doing var directiveScope = myElement. children(). scope() - It will get the element's child (the directive itself), and get the scope for that. For testing timeouts, you can use $timeout.

Which component is used to inject and mock AngularJS service within the unit test?

AngularJS also provides the ngMock module, which provides mocking for your tests. This is used to inject and mock AngularJS services within unit tests.


1 Answers

I'm a beginner in testing Angular, but I've setup a plnkr that tests a very similar setup to yours with a sucessfull "second" then/promise call

http://plnkr.co/edit/kcgWTsawJ36gFzD3CbcW?p=preview

The below code snippets are slightly simplified versions of the above plnkr.

The key points I've found are

  • I note the function traverseTheStuff doesn't call $http/$httpBackend at all. It only uses functions defined in $q promises, so the testing on assumes use of $q, and injects that

    var deferred1 = null;
    var deferred2 = null;
    var $q = null;
    
    beforeEach(function() {
      inject(function(_$q_) {
        $q = _$q_;
      });
    });
    
    beforeEach(function() {
      deferred1 = $q.defer();
      deferred2 = $q.defer();
    }
    
  • The functions to be called asynchronously are spied/stubbed with their promise return values, where the promise is created in the test itself, so their actual implementation isn't called when testing traverseTheStuff

    spyOn(MyService,'traverseTheStuff').andCallThrough();
    spyOn(MyService,'getRootData').andReturn(deferred1.promise);
    spyOn(MyService,'getNextLevel').andReturn(deferred2.promise);
    spyOn(MyService,'aggregateTheStuff');
    
  • There aren't any calls to "then" in the test, only to "resolve" on the promises created in the test, followed by $rootScope.$apply(), to then actually call the "then" callbacks in traverseTheStuff, which we can also test are called

    beforeEach(function() {
      spyOn(deferred1.promise, 'then').andCallThrough();
    });
    
    beforeEach(function() {
      deferred1.resolve(testData);
      $rootScope.$apply(); // Forces $q.promise then callbacks to be called
    });
    
    it('should call the then function of the promise1', function () { 
      expect(deferred1.promise.then).toHaveBeenCalled();
    });
    
  • Each promise must be resolved/$apply-ed to call the next "then" function in the chain. So. to get the test to call aggregateTheStuff (or rather, its stub), the second promise, returned from the getNextLevel stub, must also be resolved:

    beforeEach(function() {
      deferred2.resolve(testLevel);
      $rootScope.$apply(); // Forces $q.promise then callbacks to be called
    });
    
    it('should call aggregateTheStuff with ' + testLevel, function () {
      expect(MyService.aggregateTheStuff).toHaveBeenCalledWith(testLevel);
    });
    

An issue with all of the above, is that it assumes certain behaviour from $q and $rootScope. I was under the understanding unit tests like this shouldn't make this assumptions, in order to truly only test one bit of code. I've not worked out how to get around this, or if I'm misunderstanding.

like image 70
Michal Charemza Avatar answered Oct 30 '22 03:10

Michal Charemza