48

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();
  }); 
Community
  • 1
  • 1
Terry
  • 14,099
  • 9
  • 56
  • 84
  • 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. – Doug Apr 30 '15 at 18:48
  • 4
    `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)` – Igonato Jun 02 '15 at 06:27

4 Answers4

42

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

pkozlowski.opensource
  • 117,202
  • 60
  • 326
  • 286
  • 2
    It works but seems a bit ridiculous, I was hoping there was an alternative. I suppose that's an issue for the Angular team. – Terry Jun 03 '14 at 17:16
  • 6
    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. – pkozlowski.opensource Jun 03 '14 at 17:19
  • 4
    I tried this, but am now getting a 'No more request expected' error. – Blake Mar 13 '15 at 19:47
  • @pkozlowski.opensource I have an issue to get data from controller can you please look into it. Can you help me on it. https://stackoverflow.com/questions/53629138/how-to-test-and-resolve-controller-data-then-function-promise-and-get-orgin – Ajay.k Dec 06 '18 at 13:33
12

there is always the $rootScope, use it

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

myRootScope.$digest();
mpm
  • 20,148
  • 7
  • 50
  • 55
  • 1
    succinct - maybe too succinct but useful nonetheless – danday74 Mar 24 '16 at 13:16
  • 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 }) – Janey Jun 21 '18 at 10:40
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.

Community
  • 1
  • 1
  • ```$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 – paulhhowells Aug 10 '15 at 17:14
  • 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. – Jeffrey A. Gochin Aug 11 '15 at 02:46
  • As a followup to this. I have started using Protractor and Selenium to automate my test harness. – Jeffrey A. Gochin Jul 15 '16 at 00:28
3

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);
}));
Rentering.com
  • 399
  • 3
  • 9
  • 1
    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? – Anthony Joanes Mar 03 '16 at 10:57