Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 272
Brian Triplett Avatar asked Mar 04 '16 22:03

Brian Triplett


People also ask

How do you test promise reject Jasmine?

describe('test promise with jasmine', function() { it('expects a rejected promise', function() { var promise = getRejectedPromise(); // return expect(promise). toBe('rejected'); return expect(promise. inspect(). state).

What is promise in Jasmine?

Promises. If you need more control, you can explicitly return a promise instead. Jasmine considers any object with a then method to be a promise, so you can use either the Javascript runtime's built-in Promise type or a library.

Does Jasmine support asynchronous operations?

Jasmine has a built-in way to handle async code and that's by the passed in done function in the test specs. The Jasmine test spec function is passed a function as the first param, we usually call this parameter done .


2 Answers

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.

like image 118
Estus Flask Avatar answered Oct 20 '22 01:10

Estus Flask


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();
    });  
});
like image 42
Tate Thurston Avatar answered Oct 20 '22 01:10

Tate Thurston