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.
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).
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.
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 .
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();
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With