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.
-
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 insideinject
. – Estus Flask Commented Mar 5, 2016 at 23:53 -
1
Adding
scope.$apply()
to the first line of myit
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
3 Answers
Reset to default 6Angular 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.