Blog

Testing Promises in AngularJS

12 Oct, 2013

I like how testable AngularJS makes your code. But when you are testing code that makes use of promises, things can still be tricky.
I ran into a case where my test didn’t work and I didn’t understand why. Some understanding of how Angular has integrated Kris Kowal’s Q promises framework was required to figure out what was going on, and to get the test to work.

The code

My code consists of an AccountController which controls a simple account registration dialog. Users can submit their screenname and their realname, where upon the AccountController will make an asynchronous call to a userService, which will do the real work.
When the userService succeeds in creating the account, the user is relocated to a success page: if something goes wrong, an errormessage is added to the scope.
Note that in line 6, when asked to create an account, the userService does not immediately calls the success- or error callback, but it returns a promise.

Here’s the AccountController:

[code language=”javascript”]
angular.module(‘myApp’)
.controller(‘AccountController’, function ($scope, $location, userService) {
$scope.submit = function () {
$scope.error = null;

userService.createAccount($scope.screenname, $scope.realname).then(
function success() {
$location.path(‘/congratulations’);
},
function failed() {
$scope.error = ‘Could not create account, sorry’;
});
};
});
[/code]

And here is my initial Jasmine test

[code language=”javascript”]
describe(‘Controller: CreateAccountCtrl’, function () {
beforeEach(module(‘xtweetApp’));

var CreateaccountCtrl, scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
scope.screenname = ‘misja’;
scope.realname = ‘misja alma’;

CreateaccountCtrl = $controller(‘CreateAccountCtrl’, {
$scope: scope
});
}));

it(‘submit should call the userService with screenname and realname’, inject(function ($q, userService, $location) {
// Return a successful promise from the userService
var deferredSuccess = $q.defer();
spyOn(userService, ‘createAccount’).andReturn(deferredSuccess.promise);

spyOn($location, ‘path’);

scope.submit();

expect(userService.createAccount).toHaveBeenCalled();
expect(userService.createAccount).toHaveBeenCalledWith(‘misja’, ‘misja alma’);

deferredSuccess.resolve(); // resolves the promise

expect(scope.error).toBe(null);
expect($location.path).toHaveBeenCalledWith(“/congratulations”); // This fails!
}));
});
[/code]

It fails, with the message:
[code]
Error: Expected spy path to have been called with [ ‘/congratulations’ ] but it was never called.
[/code]

It appears that the success handler of the promise that was returned by the userService was never called. And neither was the error handler, because no error message had been put on the scope.
But why?

Angular’s digest cycle

To understand this, we need a little overview of Angular’s digest cycle.
Here’s a diagram showing Angular’s event loop:

Angular Digest Cycle

The diagram shows that when an event is received from the browser, Angular event handlers will be called within a apply() function call.
The apply function places the callback on the evalAsync queue and then invokes a call to digest().
It is the digest function that processes all callbacks on the evalAsync queue, checks if any watched variables have been modified, and if necessary processes the evalAsync queue again.
A longer explanation can be found here

Now how does this effect our promise?

When a promise is resolved, its callbacks are not called immediately. Instead,
Angular puts the promise.resolve callbacks on the evalAsync queue. There they wait until the queue will be processed. Only then the callbacks will be called.
And it is the above mentioned digest() method that processes the queue.

The solution

So, to get my test to work, I have to insert a call to digest after my promise is resolved, like so:

[code language=”javascript”]
it(‘submit should call the userService with screenname and realname’, inject(function ($q, userService, $location) {
var deferredSuccess = $q.defer();
spyOn(userService, ‘createAccount’).andReturn(deferredSuccess.promise);

spyOn($location, ‘path’);

scope.submit();

expect(userService.createAccount).toHaveBeenCalled();
expect(userService.createAccount).toHaveBeenCalledWith(‘misja’, ‘misja alma’);

deferredSuccess.resolve();
scope.$digest(); // This makes sure that all callbacks of promises will be called

expect(scope.error).toBe(null);
expect($location.path).toHaveBeenCalledWith(“/congratulations”);
}));
});
[/code]

And now the test passes!

Qxperts. We empower companies to deliver reliable & high-quality software. Any questions? We are here to help! www.qxperts.io

guest
10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ken Sodemann
Ken Sodemann
8 years ago

Thank-you for posting that. I was struggling with that exact same thing for quite a bit.

bm
bm
7 years ago

Thanks!!! It was very helpfully!!!

Thalapathy K
Thalapathy K
7 years ago

Very good explanation! Thanks!! Better than most of what I read on the web.

naveen
naveen
7 years ago

Thanks! very helpful

Lucas
Lucas
7 years ago

Thanks Misja! I was really lost on promise handling for unit tests. This helped me a lot.

Some dude
Some dude
6 years ago

In your controller you reference userService.createAccount, later you reference userService.createUser. That’s a mistake right? I hope, cause I’m barely hanging on here. Thanks for the post. This stuff is so confusing.

Edy Segura
Edy Segura
6 years ago

How can we do this kind of test in a angularjs service instead of controller?

Edy Segura
Edy Segura
6 years ago
Reply to  Edy Segura

I’ve injected $rootScope into service and it works!! Thank you for all!

Chad Wagner
Chad Wagner
6 years ago

Thank you! And for folks having issues in a service, as @edy suggested:
“`
var $rootScope, $q;
inject(function ( _$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q;
});
//…
//then in your test:
it(“Should resolve the promise once video loads”, function () {
var a = ‘nothing’;
var deferred = $q.defer();
deferred.promise.then(function(){
a = ‘something’;
});
deferred.promise.resolve();
alert(a);// still ‘nothing’
$rootScope.$digest();
alert(a);// promise.then() got called, so now a= ‘something’
});
“`

Explore related posts