1

How do I run a test with a mocked angular and a promise that is resolved after some time?

Put more simply: the test below will never run

var injector = angular.injector(['ngMock']);
var scope = injector.get('$rootScope').$new();
var q = injector.get('$q');

var promise = function() {
  return q(function(resolve, reject) {
    setTimeout(function() {
      resolve();
    }, 500);
  });
};

promise()
  .then(function() {
    document.getElementById('result').innerHTML = 'TEST RUN';
  });

//resolve the promises
scope.$digest();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-mocks.js"></script>
<p id="result">starting test...</p>
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
dariosalvi
  • 405
  • 4
  • 13
  • 1
    Do you need to have the timeout actually run for 500ms (or can it just be 1 ms 1000000ms?), or just evaluate from a timeout, and have the promises resolve? – mikeswright49 Aug 12 '15 at 14:48
  • the timeout is just an example, in fact it can be solved easily using $timeout instead of setTimeout. The point is that, whenever $q is used asynchronously, you cannot test it unless you know when to call scope.$digest(); which is not always known in advance. – dariosalvi Aug 13 '15 at 10:16
  • Why are you testing non-angular code with angular? – aaaaaa Aug 13 '15 at 20:38
  • because I am making some non-angular code, angular friendly. Concretely I am integrating a Cordova plugin with Ionic. I have checked what these guys do in [ngcordova](http://ngcordova.com/) and they also use $q promises (see an [example](https://github.com/driftyco/ng-cordova/blob/master/src/plugins/sms.js)). In their tests, they do indeed call `$rootScope.$digest();` (see [this](https://github.com/driftyco/ng-cordova/blob/master/test/plugins/sms.spec.js)), but that wouldn't work in my case :( – dariosalvi Aug 14 '15 at 08:58

3 Answers3

2

Ok I've got it.

The solution is actually the one proposed by @mido22, but I prefer a slightly different version, which you can derive from the documentation of $scope and inprog.

According to the angular documentation, you need to call $apply() when you are working in asynchronous functions like, it says explicitly, setTimeout. Here the explanation is better:

code that is being trigger directly as a call back to some external event, from the DOM or 3rd party library, should expect that it is never called from within Angular, and so any Angular application code that it calls should first be wrapped in a call to $apply."

So the solution is to wrap all calls that would affect Angular (including resolve() and reject()) into an $apply.

To avoid inprog errors, one must not call $apply() inside another. It's not the case in this example, but, supposing we had another nested setTimeout, only the last to be called should call $apply().

UPDATE:

according to this, the best way to avoid inprog errors is to wrap the non-angular code inside a $timeout(). It's the strategy recommended by angular guys.

var injector = angular.injector(['ngMock']);
var scope = injector.get('$rootScope').$new();
var q = injector.get('$q');

var promise = function() {
  return q(function(resolve, reject) {
    setTimeout(function() {
      scope.$apply(function() {
        resolve();
      });
    }, 500);
  });
};

promise()
  .then(function() {
    document.getElementById('result').innerHTML = 'TEST RUN';
  });
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-mocks.js"></script>
<p id="result">starting test...</p>
Community
  • 1
  • 1
dariosalvi
  • 405
  • 4
  • 13
1

apparantly, $q is integrated with the $rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI. ( for more details, check differences between Q and $q in docs). I have just added scope.$apply() to your snippet:

var injector = angular.injector(['ngMock']);
var scope = injector.get('$rootScope').$new();
var q = injector.get('$q');

var promise = function() {
  return q(function(resolve, reject) {
    setTimeout(function() {
      resolve();
      scope.$apply();
    }, 500);
  });
};

promise()
  .then(function() {
    document.getElementById('result').innerHTML = 'TEST RUN';
  });

//resolve the promises
scope.$digest();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular-mocks.js"></script>
<p id="result">starting test...</p>
mido
  • 24,198
  • 15
  • 92
  • 117
  • thanks for your suggestion, but I think it's not acceptable for two reasons: 1) the function "promise" is the one under test and should not mix its logic with the test's one, 2) I have tried this approach in my code, and I get an [inprog](https://docs.angularjs.org/error/$rootScope/inprog) errors – dariosalvi Aug 13 '15 at 10:08
  • @dariosalvi you are getting inprog error because it(`scope.$apply();`) is not safe, one way to beat that is, check status before you do it, another way to do things is use browser's default Promise( and it's polyfill for !@#$ like IE), any particular reason for using `$q` here? – mido Aug 13 '15 at 12:11
  • I am using $q because it's the standard angular promise implementation, no need to import anything else, and no worries about different APIs. Maybe, when mocked, you just can't use $q this way, which is a bit bad for testability. – dariosalvi Aug 13 '15 at 17:24
0

So you shouldn't use the setTimeout function with angular instead you can use the $timeout service provided by angular. Within a unit test things like timeouts are not going to be able to effectively calculated and controlled like they would in a page. In order to resolve the timeouts you would use $timeout.flush() and instead do it like this:

var injector = angular.injector(['ngMock']);
var scope = injector.get('$rootScope').$new();
var q = injector.get('$q');
var timeout = injector.get('$timeout');

var promise = function() {
  return q(function(resolve, reject) {
    timeout(function() {
      resolve();
      scope.$apply();
    }, 500);
  });
};

promise()
  .then(function() {
    document.getElementById('result').innerHTML = 'TEST RUN';
  });

//resolve the promises
timeout.flush();
scope.$digest();
mikeswright49
  • 3,381
  • 19
  • 22
  • thanks for the suggestion. As I said in my first comment, the timeout is just an example, I know that I can use $timeout instead, but it's not my point. My point is: how do I test with $q when I have some asyncrhonous code? To be specific, in my concrete case I have a set of calls to Cordova plugins, so there's no angular $-something I can use. – dariosalvi Aug 13 '15 at 17:20