How to resolve promises in AngularJS, Jasmine 2.0 when there is no $scope to force a digest?

27,110

Solution 1

You need to inject $rootScope in your test and trigger $digest on it.

Solution 2

there is always the $rootScope, use it

inject(function($rootScope){
myRootScope=$rootScope;
})
....

myRootScope.$digest();

Solution 3

So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.

First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.

Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.

I hope this helps the next person that comes a long that has is same issue.

Update


I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.

This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.

something.on('ready', function(err) {
    $rootScope.$apply(function(){deferred.resolve()});              
});

Note that it may need to be wrapped in a $timeout in some case.

something.on('ready', function(err) {
    $timeout(function(){
      $rootScope.$apply(function(){deferred.resolve()});    
    });     
});

One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.

Solution 4

From the angular documentation.

https://docs.angularjs.org/api/ng/service/$q

it('should simulate promise', inject(function($q, $rootScope) {
  var deferred = $q.defer();
  var promise = deferred.promise;
  var resolvedValue;

  promise.then(function(value) { resolvedValue = value; });
  expect(resolvedValue).toBeUndefined();

  // Simulate resolving of promise
  deferred.resolve(123);
  // Note that the 'then' function does not get called synchronously.
  // This is because we want the promise API to always be async, whether or not
  // it got called synchronously or asynchronously.
  expect(resolvedValue).toBeUndefined();

  // Propagate promise resolution to 'then' functions using $apply().
  $rootScope.$apply();
  expect(resolvedValue).toEqual(123);
}));
Share:
27,110

Related videos on Youtube

Terry
Author by

Terry

I build web applications for the modern enterprise. I value efficiency, stability, performance & elegance. GitHub Twitter

Updated on September 26, 2020

Comments

  • Terry
    Terry almost 4 years

    It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).

    The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.

    app.service('myService', function($q) {
      return {
        getSomething: function() {
          var deferred = $q.defer();
          deferred.resolve('test');
          return deferred.promise;
        }
      }
    });
    

    describe('Method: getSomething', function() {
      // In this case the expect()s are never executed
      it('should get something', function(done) {
        var promise = myService.getSomething();
    
        promise.then(function(resp) {
          expect(resp).toBe('test');      
          expect(1).toEqual(2);
        });
    
        done();
      });
    
      // This throws an error because done() is never called.
      // Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
      it('should get something', function(done) {
        var promise = myService.getSomething();
    
        promise.then(function(resp) {
          expect(resp).toBe('test');      
          expect(1).toEqual(2);
          done();
        });
      });
    });
    

    What is the correct way to test this functionality?

    Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.

      it('should get something', function($rootScope, done) {
        var promise = myService.getSomething();
    
        promise.then(function(resp) {
          expect(resp).toBe('test');      
        });
    
        $rootScope.$digest();
        done();
      }); 
    
    • Doug
      Doug about 9 years
      I have an ng service with two functions which return different $q promises. In jasmine, I could not get the test for either function to work with any of your suggestions. I got jasmine timeout error. My .then(cb) handler in my test was calling the jasmine done handler as evidenced by console msgs around done call. Only thing that worked was $digest() in the service itself after the resolve. But to make things weirder, the second service function got an ng error saying that the "digest was already running". So I commented this digest out and all is good but no idea why. Now, that is silly.
    • Igonato
      Igonato about 9 years
      done call at the end of the test doesn't make sense. The test isn't really asynchronous at this point. It should be called at the end of .then anonymous function. And in case the promise is truly asynchronous itself instead of calling digest once at the end it should be something like setInterval($rootScope.$digest, 100)
  • Terry
    Terry about 10 years
    It works but seems a bit ridiculous, I was hoping there was an alternative. I suppose that's an issue for the Angular team.
  • pkozlowski.opensource
    pkozlowski.opensource about 10 years
    Well, the problem is that current implementation of promises is tied to the digest cycle (actually it is non-trivial problem to solve as we don't have an equivalent of the nextTick method in a browser). So yes, I'm afraid that it is your only option as long as propagation of promise results is tied to the $digest cycle in AngularJS.
  • Blake
    Blake over 9 years
    I tried this, but am now getting a 'No more request expected' error.
  • paulhhowells
    paulhhowells almost 9 years
    $timeout will run $apply, so I don’t believe you need to use $rootScope.$apply inside the $timeout. Try just: $timeout(function(){ deferred.resolve(); }); N.B.: I am hoping this will help @Jeffrey A. Gochin, not recommending this as a solution for the OP
  • Jeffrey A. Gochin
    Jeffrey A. Gochin almost 9 years
    You know what, after playing with this for a while, I realized that the real problem is Angular is this case. The way they implement the mocks for $timeout are what causes this issue. My own testing, I have decided to just create my own test harness in the browser. Without angular mocks. When I do this the problem goes away. The problem now is how to still automate the testing.
  • Anthony Joanes
    Anthony Joanes over 8 years
    I'm doing similar in a unit test but when I call $rootScope.$apply(); it actually triggers something in a module which I though was mocked and trys to do an http GET?
  • danday74
    danday74 over 8 years
    succinct - maybe too succinct but useful nonetheless
  • Jeffrey A. Gochin
    Jeffrey A. Gochin almost 8 years
    As a followup to this. I have started using Protractor and Selenium to automate my test harness.
  • Janey
    Janey about 6 years
    Hi I know this is old but I've just updated to AngularJS 1.7.2 and this is no longer working. I get the error: Error: Unexpected request: GET /api/auth/params error properties: Object({ $$passToExceptionHandler: true })
  • Ajay.k
    Ajay.k over 5 years
    @pkozlowski.opensource I have an issue to get data from controller can you please look into it. Can you help me on it. stackoverflow.com/questions/53629138/…