Reputation: 4248
I've been trying to wrap my head around Jasmine 2.0 and AngularJS promises. I know that:
done
function to replace the old runs
and waitsFor
functions$q
promises will not resolve until a digest cycle is triggeredHow can I test AngularJS promises using the new async syntax in Jasmine 2.0?
Upvotes: 31
Views: 13780
Reputation: 1776
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.
Upvotes: 4
Reputation: 2327
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
Upvotes: 4
Reputation: 4248
After your call to promise.resolve()
:
$timeout.flush()
. This will force a digest cycle and propagate the promise resolutiondone()
. This tells Jasmine the async tests have completedHere'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
});
});
Upvotes: 44