Jasmine unit tests not waiting for promise resolution

杀马特。学长 韩版系。学妹 提交于 2019-12-10 13:55:20

问题


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.


回答1:


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.




回答2:


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();
    });  
});



回答3:


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.



来源:https://stackoverflow.com/questions/35807119/jasmine-unit-tests-not-waiting-for-promise-resolution

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!