简体   繁体   中英

Jasmine unit tests not waiting for promise resolution

I have an angular service that has an async dependency like this

(function() {
    angular
        .module('app')
        .factory('myService', ['$q', 'asyncService',

    function($q, asyncService) {

        var myData = null;

        return {
            initialize: initialize,
        };

        function initialize(loanId){
            return asyncService.getData(id)
                .then(function(data){
                    console.log("got the data!");
                    myData = data;
            });
        }
    }]);
})();

I want to unit test the initialize function and I'm trying in jasmine like this:

describe("Rate Structure Lookup Service", function() {

    var $q;
    var $rootScope;
    var getDataDeferred;
    var mockAsyncService;
    var service;

    beforeEach(function(){
        module('app');

        module(function ($provide) {
            $provide.value('asyncService', mockAsyncService);
        });

        inject(function(_$q_, _$rootScope_, myService) {
            $q = _$q_;
            $rootScope = _$rootScope_;
            service = myService;
        });

        getDataDeferred = $q.defer();

        mockAsyncService = {
            getData: jasmine.createSpy('getData').and.returnValue(getDataDeferred.promise)
        };
    });

    describe("Lookup Data", function(){
        var data;

        beforeEach(function(){
            testData = [{
                recordId: 2,
                effectiveDate: moment("1/1/2015", "l")
            },{
                recordId: 1,
                effectiveDate: moment("1/1/2014", "l")
            }];
        });

        it("should get data", function(){
            getDataDeferred.resolve(testData);

            service.initialize(1234).then(function(){
                console.log("I've been resolved!");
                expect(mockAsyncService.getData).toHaveBeenCalledWith(1234);
            });

            $rootScope.$apply();
        });
    });
});

None of the console messages appear and the test seems to just fly on through without the promises ever being resolved. I though that the $rootScope.$apply() would do it but seems not to.

UPDATE

@estus was right that $rootScope.$appy() is sufficient to trigger resolution of all the promises. It seems that the issue was in my mocking of the asyncService. I changed it from

mockAsyncService = {
    getData: jasmine.createSpy('getData').and.returnValue(getDataDeferred.promise)
};

to

mockAsyncService = {
    getData: jasmine.createSpy('getData').and.callFake(
        function(id){
            return $q.when(testData);
    })
};

and I set testData to what I need to for the tests rather than calling getDataDeferred.resolve(testData) . Prior to this change, the mockAsyncService was being injected but the promise for getDataDeferred was never being resolved.

I don't know if this is something in the order of injection in the beforeEach or what. Even more curious was that is has to be a callFake . Using .and.returnValue($q.when(testData)) still blocks promise resolution.

Angular promises are synchronous during tests, $rootScope.$apply() is enough to make them settled at the end of the spec.

Unless asyncService.getData returns a real promise instead of $q promise (and it doesn't in this case), asynchronicity is not a problem in Jasmine.

Jasmine promise matchers library is exceptionally good for testing Angular promises. Besides the obvious lack of verbosity, it provides valuable feedback in such cases. While this

rejectedPromise.then((result) => {
  expect(result).toBe(true);
});

spec will pass when it shouldn't, this

expect(pendingPromise).toBeResolved();
expect(rejectedPromise).toBeResolvedWith(true);

will fail with meaningful message.

The actual problem with the testing code is precedence in beforeEach . Angular bootstrapping process isn't synchronous.

getDataDeferred = $q.defer() should be put into inject block, otherwise it will be executed before the module was bootstrapped and $q was injected. The same concerns mockAsyncService that uses getDataDeferred.promise .

In best-case scenario the code will throw an error because defer method was called on undefined . And in worst-case scenario (which is the reason why spec properties like this.$q are preferable to local suite variables) $q belongs to an injector from the previous spec, thus $rootScope.$apply() will have no effect here.

You need to pass the optional done parameter to the callback function in your it block. Otherwise jasmine has no way of knowing you're testing an async function -- async functions return immediately.

Here's the refactor:

it("should get data", function(done){

    service.initialize(1234).then(function(){
        console.log("I've been resolved!");
        expect(mockAsyncService.getData).toHaveBeenCalledWith(1234);
        done();
    });  
});

Here are some (flaky, back-of-the-beer-mat) pointers. Unfortunately I have no way of knowing if they are actual mistakes or whether they are "typos" because you "simplified" the code.

First of all, there's no reason not to provide the asyncService as a service, and inline. Try this:

$provide.service('asyncService', function() {
    // asyncService implementation
});

Also, I don't believe that this dependency injection would work.

inject(function(_$q_, _$rootScope_, myService) {
    $q = _$q_;
    $rootScope = _$rootScope_;
    service = myService;
});

Because the DI container doesn't know about myServiceProvider. You could try this instead:

inject(function(_$q_, _$rootScope_, _asyncService_) {
    $q = _$q_;
    $rootScope = _$rootScope_;
    service = _asyncService_;
});

Which would work because you called $provide earlier with 'asyncService' as a parameter.

Also, you're not using the $promise api properly. You're not returning a resolve()'d promise to the .then() in your unit test. Try using an alternate implementation for asyncService similar to this:

$provide.service('asyncService', function() {
    this.getData = function() {
        return $q(function(resolve, reject) {
         resolve('Promise resolved');
        });
    }
});

Check the docs for $q

You could spy on this in your unit test like this. There's no reason to call the spy in your beforeEach() function.

jasmine.spyOn(service, 'getData').and.callThrough();

Your expect() looks good.

Let me know if any of this helps you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM