I've been trying to wrap my head around Jasmine 2.0 and AngularJS promises. I know that:
- Jasmine 2.0 introduced the
done
function to replace the old runs
and waitsFor
functions
- AngularJS
$q
promises will not resolve until a digest cycle is triggered
How can I test AngularJS promises using the new async syntax in Jasmine 2.0?
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
});
});
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 digest
on 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
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.