Mocking asynchronous service function with angular / karma / jasmine
The test in this code fails. I cannot successfully test the return of an async function.
describe('mocking services', function () {
var someService, deferred;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function (_someService_) {
someService = _someService_;
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
expect(retVal).toEqual(44);
});
});
I get the following error on startup:
Chrome 36.0.1985 (Mac OS X 10.9.4) mocking services should be able to test values from both functions FAILED
Expected undefined to equal 44.
Error: Expected undefined to equal 44.
at null.<anonymous> (/Users/selah/WebstormProjects/macrosim-angular/test/spec/services/usersAndRoles-service-test.js:34:24)
How can I take this test?
source to share
When mocking async calls with $ q, you need to use $rootScope.$apply()
because of the way $ q is implemented.
In particular, a method is .then
not called synchronously, it must always be asynchronous, regardless of whether it was called synchronous or asynchronous.
For this, $ q integrates with $ rootScope. Therefore, in your unit tests, you need to notify $ rootScope that something has changed (i.e. - start the digest loop). To do this, you call$rootScope.$apply()
See here (in particular, "Differences between Kris Kowal Q and $ q Section")
The working code looks like this:
describe('mocking services', function () {
var someService, deferred, rootScope;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function ($injector) {
someService = $injector.get('someService');
rootScope = $injector.get('$rootScope');
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
rootScope.$apply();
expect(retVal).toEqual(44);
});
});
source to share
$q
the deferred decision is still executed asynchronously.
A quick test, albeit in an older Angular version: http://plnkr.co/edit/yg2COXG0TWBYniXOwJYb
This test should work:
it('should be able to test values from both functions', function (done) {
expect(someService.trySynch()).toEqual(33);
someService.tryAsynch().then(function(r){
expect(r).toEqual(44);
done();
});
deferred.resolve(44);
});
source to share
If I run rootScope.$apply()
before an expect clause that tests my async function, then the test succeeds. Also, it fails if I supply the wrong value as I would expect.
So my test is functional, but I however don't understand why rootScope. $ apply () is very important here, so if anyone wants to copy my code and provide an explanation, I will happily mark your answer as the correct answer!
My working test code looks like this:
describe('mocking services', function () {
var someService, deferred, rootScope;
beforeEach(function () {
module(function($provide){
$provide.factory('someService', function($q){
return{
trySynch: function(){
return 33;
},
tryAsynch: function(){
deferred = $q.defer();
return deferred.promise;
}
};
});
});
inject(function ($injector) {
someService = $injector.get('someService');
rootScope = $injector.get('$rootScope');
});
});
it('should be able to test values from both functions', function () {
expect(someService.trySynch()).toEqual(33);
var retVal;
someService.tryAsynch().then(function(r){
retVal = r;
});
deferred.resolve(44);
rootScope.$apply();
expect(retVal).toEqual(44);
});
});
source to share