In a Jasmine test for an Angular app, I stubbed a method that returns a $q promise. The test kept timing out even though I didn’t forget about done().

Versions used below

{
  "angular": "1.3.8",
  "jasmine-core": "2.1.3"
}

Digest cycle needed

Just like how a digest cycle is needed for Angular to notice changes to scope properties, a digest is needed for $q promise chain propagation.

When necessary, you can trigger a digest cycle by calling $rootScope.$apply().

Test with timeout fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
describe('beginGame()', function() {
  var gameService, $rootScope;

  beforeEach(inject(function(_gameService_, _$rootScope_, $q) {
    gameService = _gameService_;
    $rootScope = _$rootScope_;

    // Stub method so it just returns a resolved promise.
    // The controller being tested uses gameService.
    spyOn(gameService, 'start').and.callFake(function(game) {
      return $q(function(resolve, reject) {
        resolve(game);
      });
    });
  }));

  it('starts a game with given players', function(done) {
    controller.addPlayers(['jill', 'joe', 'jen']);

    // here's the $q promise chain
    controller.beginGame()
      .then(assertGameStarted)
      .finally(done);

    function assertGameStarted() {
      expect(gameService.start).toHaveBeenCalledWith(jasmine.objectContaining({
        playerNames: ['jill', 'joe', 'jen']
      }));
    }

    // manually trigger digest cycle
    $rootScope.$apply();
  });
});

Without line 32 the promise chain doesn’t propagate, done() is never called and the test fails due to timeout.

Why $q works fine in app code

Digest cycles are triggered for you behind the scenes. Angular built-ins (ngClick, $timeout, etc) call $rootScope.$apply() which triggers a digest cycle.