Reputation: 4211
If you are testing code in Angular which uses $q and immediately resolves such as;
angular.module('MyApp.myModule', ['ng'])
.service('someService', function($q) {
this.task = function() {
var deferred = $q.defer();
deferred.resolve('some value');
return deferred.promise;
};
});
That might be used as follows;
function(someService) {
someService.task().then(function() {
console.log('resolved');
});
}
You might find that it runs as expected in your application, but fails under test;
PhantomJS 1.9.7 (Mac OS X) MyApp.myModule someService someService.task when invoked returned promise when invoked should call our handler immediately FAILED
Expected spy onTaskComplete to have been called with [ 'some value' ] but it was never called.
Here is an example test for the above module;
describe('MyApp.myModule', function() {
describe('someService', function() {
beforeEach(function() {
var suite = this;
module('MyApp.myModule');
suite.injectService = function() {
inject(function(someService) {
suite.someService = someService;
});
};
});
describe('when instantiated', function() {
beforeEach(function() {
this.injectService();
});
it('should expose the expected API', function() {
expect(typeof this.someService.task).toEqual('function');
});
});
describe('someService.task', function() {
describe('when invoked', function() {
beforeEach(function() {
this.injectService();
this.taskPromise = this.someService.task();
});
it('should return a promise', function() {
expect(typeof this.taskPromise.then).toEqual('function');
});
describe('returned promise', function() {
describe('when invoked', function() {
beforeEach(function() {
this.onTaskComplete = jasmine.createSpy('onTaskComplete');
this.taskPromise.then(this.onTaskComplete);
});
it('should call our handler immediately', function() {
expect(this.onTaskComplete).toHaveBeenCalledWith('some value');
});
});
});
});
});
});
});
Upvotes: 3
Views: 1833
Reputation: 4211
The reason this fails is that—although the code appears to be synchronous—internally $q
uses $evalAsync
from $scope
to defer the work until a future call stack. Since $q
has no flush
method like $httpBackend
, $timeout
, and $interval
do - a call to $rootScope.$digest()
is needed to achieve the same result.
PhantomJS 1.9.7 (Mac OS X): Executed 3 of 3 SUCCESS (0.451 secs / 0.01 secs)
Here is the updated example test;
describe('MyApp.myModule', function() {
describe('someService', function() {
beforeEach(function() {
var suite = this;
module('MyApp.myModule');
inject(function($rootScope) {
suite.$rootScope = $rootScope;
});
suite.injectService = function() {
inject(function(someService) {
suite.someService = someService;
});
};
});
describe('when instantiated', function() {
beforeEach(function() {
this.injectService();
});
it('should expose the expected API', function() {
expect(typeof this.someService.task).toEqual('function');
});
});
describe('someService.task', function() {
describe('when invoked', function() {
beforeEach(function() {
this.injectService();
this.taskPromise = this.someService.task();
});
it('should return a promise', function() {
expect(typeof this.taskPromise.then).toEqual('function');
});
describe('returned promise', function() {
describe('when invoked', function() {
beforeEach(function() {
this.onTaskComplete = jasmine.createSpy('onTaskComplete');
this.taskPromise.then(this.onTaskComplete);
this.$rootScope.$digest();
});
it('should call our handler immediately', function() {
expect(this.onTaskComplete).toHaveBeenCalledWith('some value');
});
});
});
});
});
});
});
Upvotes: 5