Question

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');
            });
          });
        });
      });
    });
  });
});
Was it helpful?

Solution

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');
            });
          });
        });
      });
    });
  });
});
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top