31
votes

I've been trying to wrap my head around Jasmine 2.0 and AngularJS promises. I know that:

How can I test AngularJS promises using the new async syntax in Jasmine 2.0?

3
If testing promises, why do you need to use the async syntax at all, both in old and new Jasmine? Can you post a function that you're trying to test? For a lot of cases, you can use $timeout.flush(), and/or myPromise.resolve() in tests to force them to be synchronous.Michal Charemza
@MichalCharemza To my surprise, $timeout.flush() works great without the use of Jasmine's done; I don't have a function to demonstrate when I would need done + $timeout.flush(). What if the tests use e.g. an actual $http backend? It's (obviously) better to mock $http for speed, but would $timeout.flush() without done work there? Does the delayed resolution of the promise make any difference?Tyler Eich
If the tests use an actual $http backend, connecting to a real server, then the test would be asynchronous, and you have to use done. $timeout.flush() (or $httpBackend.flush()) affects the code running locally: you can't call a function to demand that a server respond to the request right now! If you're not sure about how to test a specific function (say, using $timeout or $http), then you can post that function in a question).Michal Charemza

3 Answers

44
votes

After your call to promise.resolve():

  • Call $timeout.flush(). This will force a digest cycle and propagate the promise resolution
  • Call done(). This tells Jasmine the async tests have completed

Here's an example (Demo on Plunker):

describe('AngularJS promises and Jasmine 2.0', function() {
    var $q, $timeout;

    beforeEach(inject(function(_$q_, _$timeout_) {
        // Set `$q` and `$timeout` before tests run
        $q = _$q_;
        $timeout = _$timeout_;
    }));

    // Putting `done` as argument allows async testing
    it('Demonstrates asynchronous testing', function(done) {
        var deferred = $q.defer();

        $timeout(function() {
            deferred.resolve('I told you I would come!');
        }, 1000); // This won't actually wait for 1 second.
                  // `$timeout.flush()` will force it to execute.

        deferred.promise.then(function(value) {
            // Tests set within `then` function of promise
            expect(value).toBe('I told you I would come!');
        })
        // IMPORTANT: `done` must be called after promise is resolved
        .finally(done);

        $timeout.flush(); // Force digest cycle to resolve promises
    });
});
4
votes

For me the $timeout.flush() didn't work very well, but I've multiple async calls in my spec. I found the $rootScope.$apply(), as a method to force the digeston each async call.

describe('AngularJS promises and Jasmine 2.0', function () {
  beforeEach(inject(function (_$q_, _$timeout_, _$rootScope_) {
    $q = _$q_
    $timeout = _$timeout_
    $rootScope = _$rootScope_
  }))

  it('demonstrates asynchronous testing', function (done) {
    var defer = $q.defer()

    Async.call()
    .then(function (response) {
      // Do something

      var d = $q.defer()
      Async.call()
      .then(function (response) {
        d.resolve(response)
        $rootScope.$apply() // Call the first digest 
      })
      return d.promise
    })
    .then(function (response) {
      // Do something after the first digest

      Async.call()
      .then(function (response) {
        defer.resolve(response) // The original defer
        $rootScope.$apply() // Call the second digest
      })
    })

    defer.promise.then(function(value) {
      // Do something after the second digest
      expect(value).toBe('I told you I would come!')
    })
    .finally(done)

    if($timeout.verifyNoPendingTasks())
      $timeout.flush() 
  })
})

It is like a chained async calls thing. Hope it helps the conversation. Regards

4
votes

This answer won't add anything new to those of above, it is only intended to articulate the answer in more detailed way, as it worked for me. When I occurred the issue described in a question above, I spent much time tryng to find a way to make sure all promises had their time to finish and all assertions were asserted.

In my case I had a chain of promises, and after each of them I need to ensure the results do match my expectation. I did not create any promise using deferred, I rather invoked the existing ones.

So, the thing is that $timeout.flush() was completely enough for me. My working test looks like this:

describe("Plain command without side-effects", function() {
    it("All usecases", inject(function($timeout) {
        console.log("All together");
        expect(state.number).toEqual(1);
        cmdHistory
            .execute(increaseState, decreaseState)
            .then(function() {
                console.log("Execute works");
                expect(state.number).toEqual(2);
                return cmdHistory.redo(); // can't redo, nothing's undone
            })
            .then(function() {
                console.log("Redo would not work");
                expect(state.number).toEqual(2);
                return cmdHistory.undo();
            })
            .then(function() {
                console.log("Undo undoes");
                expect(state.number).toEqual(1);
                return cmdHistory.undo();
            })
            .then(function() {
                console.log("Next undo does nothing");
                expect(state.number).toEqual(1);
                return cmdHistory.redo(); // but still able to redo

            })
            .then(function() {
                console.log("And redo redoes neatly");
                expect(state.number).toEqual(2);
            });

        $timeout.flush();
    }));

This test is dedicated to make sure that commandHistory object works fine, it has to actions: execute and unExecute, and three methods: execute, undo, redo, all of which return promises.

Without $timeout.flush(), all I had in log output was All together, and no further log messages. Adding $timeout.flush() has fixed everything up, and now I have all messages shown and all assertions executed

UPDATE There's another option: you can write your test suite without chaining promises with then, but simply flushing after each promise has been called, so that to make sure it completes:

    it("All usecases 2", inject(function($timeout) {
        console.log("All usecases 2");
        expect(state.number).toEqual(1);

        console.log("Execute works");
        cmdHistory.execute(increaseState, decreaseState);
        $timeout.flush();
        expect(state.number).toEqual(2);

        console.log("Redo would not work");
        cmdHistory.redo(); // can't redo, nothing's undone
        $timeout.verifyNoPendingTasks();
        expect(state.number).toEqual(2);

        console.log("Undo undoes");
        cmdHistory.undo();
        $timeout.flush();
        expect(state.number).toEqual(1);

        console.log("Next undo does nothing");
        cmdHistory.undo();
        $timeout.verifyNoPendingTasks();
        expect(state.number).toEqual(1);

        console.log("And redo redoes neatly");
        cmdHistory.redo(); // but still able to redo
        $timeout.flush();
        expect(state.number).toEqual(2);
    }));

Please pay attention to the fact in some cases, when my methods like undo and redo do not return promise, I call $timeout.verifyNoPendingTasks() instead of flush. Which is hard to say if it's good or bad.

Yet in this case test looks more reasonable and much simpler.