Reply to comment

Testing Angular Promises and the $http XHR Service with Jasmine

This is a rough note, because I finally figured this out after several hours of trying. If I'm wrong, please email me, and I'll fix it.

I had a problem because I wanted to centralize my $http requests in services rather than having XHR calls in controllers. My first attempts used callbacks, but I wanted to convert them to promises, which are a lot nicer. Thus, I had to test promises that used $http. There weren't any tutorials about this, so I wrote this.

Read this first: A tip for Angular Unit Tests with Promises

Then review $httpBackend in ngMock, especially the examples: $httpBackend

Then, review the Jasmine docs about async testing, with the caveat that we should do the opposite of what they provide. Then read about spies. Jasmine Docs

We need to combine a little bit from the first two articles, and avoid the info in the Jasmine docs about async testing, but use the Jasmine spy features.

The first article was about testing Angular's $q promises with Jasmine. The first, most important, fact: AngularJS's promises are processed in Angular's $digest loop. Therefore, to resolve promises, we need to call $rootScope.$digest().

The second article was about testing functions that call $http with Jasmine. The important point is that ngMock replaces $httpBackend with a fake service that is programmed to present canned responses to specific requests. This allows testing without making XHR calls. When using $httpBackend, you need to call the $httpBackend.flush() method to process requests to $http.

To test Angular promises that call $http, we need to call $httpBackend.flush() and $rootScope.$digest().

Additionally, if you learned Jasmine async testing, you know about "done". To perform async Angular tests, you do not use done.

Repeat: we will not use "done" in our tests.

Additionally, I took the advice in the first article and didn't put the expectations in a promise; I used a spy instead.

A spy is Jasmine's version of mock objects. A spy function can ingest the data from a promise, and then disgorge it for you, so you can examine it. You create it and use it like this:

var spy = jasmine.createSpy('success');
...
var data = spy.calls.mostRecent().args[0];

Here's the source code. The interesting stuff is at the bottom:

'use strict';

describe('Service: EbuLogin2', function () {

    // load the service's module
    beforeEach(module('publicApp'));

    // instantiate service
    var EbuLogin;
    var $injector;
    var $httpBackend;
    var $rootScope;
    beforeEach(inject(['EbuLogin2', '$injector', function (e, i) {
        EbuLogin = e;
        $injector = i;
        $rootScope = $injector.get('$rootScope');
        $httpBackend = $injector.get('$httpBackend');
        $httpBackend.when('POST', '/1/parse_login/')
            .respond({sessionToken:'fakeSessionToken'},{});
        $httpBackend.when('POST', '/1/parse_become/')
            .respond({user:'fake  user'});
    }]));

    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });

    it('should become', function() {
        var spy = jasmine.createSpy('success');
        var p = EbuLogin.become({'sessionToken':'fakeValue'})
        .then(spy);
        $httpBackend.flush();
        $rootScope.$digest();

        var data = spy.calls.mostRecent().args[0];
        expect(data.data.sessionToken).toBe('fakeSessionToken');
    });

    it('should login', function() {
        var spy = jasmine.createSpy('success');
        EbuLogin.logIn('johnk2','whatever', 'PST')
        .then(spy);
        $httpBackend.flush();
        $rootScope.$digest();

        var data = spy.calls.mostRecent().args[0];
        expect(data.data.sessionToken).toBe('fakeSessionToken');
    });
});

The interesting stuff, aka the "copy paste code" are the last two tests.

Note, again, that we don't use "done" as outlined in the Jasmine docs -- though Angular has async services, and async promises, we are manually "pumping" the Angular event loop to resolve the promises.

Like magic, it worked. I hope this saves you some time.

A few more references

Unit testing ngMock Fundamentals

Understanding Angular's $digest and $apply

Unit Testing with Angular JS

Reply

The content of this field is kept private and will not be shown publicly.
  • Lines and paragraphs break automatically.

More information about formatting options

2 + 2 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.