1

I try to test a function that uses nested promises (let's assume this function can't be changed). To process those promises I have to call $rootScope.$digest() at least two times. I came up with this working solution calling $digest() every 10ms.

// Set some async data to scope.key
// Note: This function is just for demonstration purposes.
//       The real code makes async calls to localForage.
function serviceCall(scope, key) {
  var d1 = $q.defer(), d2 = $q.defer();

  setTimeout(function () {
    d1.resolve('d1');
  });

  d1.promise.then(function (data) {
    setTimeout(function () {
      d2.resolve(data + ' - d2');
    }, 100); // simulate longer async call
  });

  d2.promise.then(function (data) {
    scope[key] = data;
  });
}

it('nested promises service', function (done) {
  var interval = setInterval(function () {
      $rootScope.$digest();
    }, 10),
    myKey = 'myKey',
    scope = {};

  serviceCall(scope, myKey);

  setTimeout(function () {
    expect(scope[myKey]).toEqual('d1 - d2');
    window.clearInterval(interval);
    done();
  }, 120); // What is the right timeout?
});

The problem is to estimate the timeout duration needed to digest enough times to resolve all promises. It gets further complicated if the service makes a real async call to e.g. localstorage.

Is there another way to solve this? Is there a way to get all remaining promises?

Reto Aebersold
  • 16,306
  • 5
  • 55
  • 74
  • In short, no, there's really no way to estimate the duration needed; there are a large number of factors that take part in this. But, I'm confused; it seems strange that you're invoking the promises directly rather than calling actual async methods - are you just trying to test the **.then()** functionality? Note: your setTimeout in your serviceCall function will have a default delay (usually 4ms). You can also use the $timeout and $interval services built into angular, this typically helps avoid needing to invoke $root.$digest() or $apply() directly. – Danny Bullis May 27 '15 at 17:32
  • Just to emphasize, your entire approach seems wrong. You really shouldn't be in a situation where you're trying to "guess" what number you should use for a timeout, especially if you're trying test something (randomness is something you should be careful with in testing) - there's ways in angular that will allow you to completely avoid having to get in the situation you're in. If you're at all interested in hearing about how you can do that, let me know and give me some more details on specifically what you're trying to test. Cheers – Danny Bullis May 27 '15 at 17:38
  • The `serviceCall()` is just a simplified example. The real code makes calls to [localForage](https://github.com/mozilla/localForage) and uses those promises / callbacks. I could inject a mock of `localForage` using `$timeout` and promises but I'd really like to test it with the `localForage` library. – Reto Aebersold May 27 '15 at 18:06
  • It looks like this, as well as the real code, just [don't use promises correctly](http://stackoverflow.com/q/23803743/1048572). Fix them and write your test for a non-nested promise. – Bergi May 27 '15 at 18:40
  • @Bergi Thanks for the hint. However the accepted answer states *"You should only use deferred objects when you are converting an API to promises"* what is what I try to do in my service around localForage as I like to wrap the callbacks as AngularJS promises. – Reto Aebersold May 27 '15 at 19:09
  • 1
    @RetoAebersold Yeah, but if you do that then you should wrap only on single API method per deferred. Make a helper function for each of the methods ([promisify the API](http://stackoverflow.com/q/22519784/1048572)) so that you only have promise-returning functions to work with. Then don't use deferreds any further, especially when combining two calls to the API like in your example. Also, it seems from the docs that [localForage already supports promises](https://github.com/mozilla/localForage#promises) out of the box, so there is no need to use any deferreds at all?! – Bergi May 27 '15 at 19:37
  • @Bergi Thanks a lot for your helpful input. I wrap the localForage promises with AngularJs ones to trigger the digest cycle to get the scope updated correctly. The question is a bit misleading as the core problem are the async calls to localForage. – Reto Aebersold May 27 '15 at 20:59

1 Answers1

2

You should be using jasmine spies to mock/stub out any external service calls so you can control when the service resolves/rejects. Your service should have tests backing up its inner functionality.

Please note this example is not perfect but abbreviated for clarity.

var service = jasmine.createSpyObj('service', ['call']);
var defServiceCall;
var $scope;

beforeEach(function () {
  module('myModule', function ($provide) {
    // NOTE: Replaces the service definition with mock.
    $provide.value('service', service);
  });
  inject(function($q, $rootScope) {
    $scope = $rootScope.$new();
    defServiceCall = $q.defer();
    service.call.and.returnValue(defServiceCall.promise);
  });
});
describe('When resolved', function () {
  beforeEach(function () {
    myFunctionCall(); // Sets myVar on the scope
    defServiceCall.resolve('data');
    $scope.$digest(); // NOTE: Must digest for promise to update
  });
  it('should do something awesome when resolved', function () {
    expect($scope.myVar).toEqual('data');
  });
});
Michael Stramel
  • 1,337
  • 1
  • 16
  • 18