最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Jasmine unit tests not waiting for promise resolution - Stack Overflow

programmeradmin4浏览0评论

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.

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.

Share Improve this question edited May 18, 2017 at 15:36 isherwood 61.1k16 gold badges120 silver badges168 bronze badges asked Mar 4, 2016 at 22:18 Brian TriplettBrian Triplett 3,5326 gold badges39 silver badges61 bronze badges 2
  • 1 Yes, I've deleted this part of my answer as irrelevant (it is race condition in beforeEach and not a deferred itself that created the problem in the first place), but using callFake with $q.when or $q.resolve allows to pick up actual $q instance and use fresh promise on every call (it matters if spy is being called more than once within the same spec). Again, both things should work as intended when being placed inside inject. – Estus Flask Commented Mar 5, 2016 at 23:53
  • 1 Adding scope.$apply() to the first line of my it method worked. $q.when made no functional difference to what I was doing, but made the method much neater. Thank you very much for both. – JonathanPeel Commented Nov 17, 2017 at 18:10
Add a ment  | 

3 Answers 3

Reset to default 6

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.

发布评论

评论列表(0)

  1. 暂无评论