Unit testing code that returns a promise

While spending some time improving code coverage on our AngularJS app recently, I came across some tests I wasn’t really happy with. The issue was that I had some services that were returning promises. My code wasn’t fully testing that this code was behaving properly. I spent a few hours looking for some pattern to follow but couldn’t find anything that I liked. I thought I’d share what I came up with here in hopes that it will be helpful to the next person looking to unit test some promises.

My Setup

As mentioned before I’m on AngularJS. Angular has a promise implementation called $q that’s based on Kris Kowal’s Q. My unit tests are written in Jasmine. As such, all my examples will be using Angular and Jasmine. That said, the concept should be applicable to any promise implementation and language. In order to not bog down with Angular specifics, I’ll skip any bootstrapping and get right to the test specs. If you need to learn about unit testing Angular you should check out the documentation or numerous other resources around the web.

The Problem

Let’s say we have a service (If you’re not familiar with Angular, what a service is isn’t important) and that service has a method that returns a promise. How do we test that this method is behaving as expected?

A Solution

My first inclination was to look at the definition of a promise. Promise-like objects are defined as those that have a then method. Such objects are described as being “thenable.”

That led me to code like this:

it('should return a promise', function() {
var returnValue = SomeService.method();
  expect(returnValue.then).toBeDefined();
// or if you want to be a little more precise
expect(typeof returnValue.then).toBe('function');
});

This confirms that the method is returning a promise but doesn’t really tell me anything about whether it resolves or rejects when it should.

Let’s see what we can do about that.

var reject;
var resolve;
beforeEach(function() {
reject = jasmine.createSpy('reject');
resolve = jasmine.createSpy('resolve');
});
it('should resolve when...', function() {
// set up mocks so that call will resolve
  SomeService.method().then(resolve, reject);
$scope.$digest();
  expect(resolve).toHaveBeenCalled();
expect(reject).not.toHaveBeenCalled();
});
it('should reject when...', function() {
// set up mocks so that call will reject
  SomeService.method().then(resolve, reject);
$scope.$digest();
  expect(resolve).not.toHaveBeenCalled();
expect(reject).toHaveBeenCalled();
});

Great. By actually calling then on the promise and spying on the handlers we can verify both that the return value of the method was thenable (and therefore a promise) but that the handlers were called when they were supposed to be.

Finally let’s make sure that the handlers are receiving the correct data.

var reject;
var rejectData;
var resolve;
var resolveData;
beforeEach(function() {
reject = jasmine.createSpy('reject');
rejectData = expectedDataWhenPromiseIsRejected;
resolve = jasmine.createSpy('resolve');
resolveData = expectedDataWhenPromiseisResolved;
});
it('should resolve with data when...', function() {
// set up mocks so that call will resolve
  SomeService.method().then(resolve, reject);
$scope.$digest();
  expect(resolve).toHaveBeenCalledWith(resolveData);
expect(reject).not.toHaveBeenCalled();
});
it('should reject with data when...', function() {
// set up mocks so that call will reject
  SomeService.method().then(resolve, reject);
$scope.$digest();
  expect(resolve).not.toHaveBeenCalled();
expect(reject).toHaveBeenCalledWith(rejectData);
});

Now we’re verifying that when the promise is resolved it’s receiving the correct data.

And that’s where I’m currently at. We have a spec that checks that the method is returning a promise by calling then on it. We utilize spies to check that the appropriate resolve or reject handler is being called. And finally we make sure that the handler gets the correct data.

Hopefully this will be of use to someone out there and saves them some time figuring this stuff out. Let me know in the comments what you think. Share and recommend if you think it’s helpful.