Promises in JavaScript Unit Tests: the Definitive Guide

Share this article

Promises
are a common part of the JavaScript code. Despite making asynchronous code simpler, dealing with promises in unit tests is a hassle. You need to wire your test’s assertions into the callbacks of the promise, which adds extra code into the test. In this way the test itself becomes a bit complicated and it’s harder to see what’s happening. In this article, I’ll show you how to fix this issue and discuss useful patterns which are able to simplify common promise-scenarios in tests’ stage. I’ve created an example project that you can download from my website which shows the techniques introduced in this article.

Getting Started

For this project I’ll use Mocha as the testing framework and the Chai library to provide the assertions. You’ll understand why in a moment. We can install the duo simply running the command:
npm install mocha chai
When you first encounter promises in unit tests, your test probably looks something like a typical unit test:
var expect = require('chai').expect;

it('should do something with promises', function(done) {
//define some data to compare against
var blah = 'foo';

//call the function we're testing
var result = systemUnderTest();

//assertions
result.then(function(data) {
expect(data).to.equal(blah);
done();
}, function(error) {
assert.fail(error);
done();
});
});
We have some test data, and call the system under test – the piece of code we’re testing. But then, the promise shows up, and the code gets complicated. For the promise, we’re adding two handlers. The first one is for a resolved promise, which has an assertion inside it to compare equality, while the second one is for a rejected promise, which has a failing assertion. We also need the done() calls in both of them. Since promises are asynchronous, we must tell Mocha this is an asynchronous test, and notify it when done. But why do we need assert.fail? The purpose of this test is to compare the result of a successful promise against a value. If the promise is rejected, the test should fail. That’s why without the failure handler, the test could report a false positive! A false positive is when a test should fail, but actually doesn’t. For instance, imagine we remove the rejection callback. Your code should look like this:
result.then(function(data) {
expect(data).to.equal(blah);
done();
});
In this case, if the promise was rejected, there would be no error, since there is no error handler in the test to check for it. But it’s clear the test should fail in that situation, as the expectation won’t run. This is definitely one of the main reasons why promises become complicated in tests.

Mocha and Promises

I decided to use Mocha in this project because it has a built-in support for promises. This means that a rejected promise will make your test fail. For example:
it('should fail the test', function() {
var p = Promise.reject('this promise will always be rejected');

return p;
});
The above test returns a rejected promise, which means that it fails every time. We can use what we’ve learnt to improve our earlier test, as shown in the following snippet:
var expect = require('chai').expect;

it('should do something with promises', function() {
var blah = 'foo';

var result = systemUnderTest();

return result.then(function(data) {
expect(data).to.equal(blah);
});
});
The test now returns the promise. We don’t need the failure handler or the done callback anymore, as Mocha handles the promise. If the promise fails, Mocha will fail the test.

Improving the Tests Further with Chai-as-promised

Wouldn’t it be nice if we could do assertions directly on promises? With chai-as-promised, we can! First, we need to install it running:
npm install chai-as-promised
We can use it like this:
var chai = require('chai');
var expect = chai.expect;

var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

it('should do something with promises', function() {
var blah = 'foo';

var result = systemUnderTest();

return expect(result).to.eventually.equal(blah);
});
We’ve replaced the entire then setup with a Chai assertion. The key here is eventually. When comparing values with Chai, we can use
expect(value).to.equal(something);
But if value is a promise, we insert eventually and return it:
return expect(value).to.eventually.equal(something)
Now, Chai deals with the promise. Note: don’t forget to return the promise, otherwise Mocha won’t know it needs to handle it! We can use any of Chai’s assertions together with eventually
. For example:
//assert promise resolves with a number between 1 and 10
return expect(somePromise).to.eventually.be.within(1, 10);

//assert promise resolves to an array with length 2
return expect(somePromise).to.eventually.have.length(2);

Useful Patterns for Promises in Tests

Comparing Objects

If your promise’s resolved value should be an object, you can use the same methods to compare as you normally would. For example, with deep.equal you can write a statement like:
return expect(value).to.eventually.deep.equal(obj)
The same warning applies here as without promises. If you’re comparing objects, equal will compare references, and make your test fail when the objects have all the same properties, but are different objects. chai-as-promised has a convenient helper for comparing objects: return expect(value).to.eventually.become(obj) Using eventually.become is the same as doing a deep equal comparison. You can use it for most equality comparisons with promises – with strings, numbers and so on – unless you specifically need a reference comparison.

Asserting Against a Specific Property from an Object

Sometimes you might want to check against only a single property in an object from a promise. Here’s one way to do it:
var value = systemUnderTest();

return value.then(function(obj) {
expect(obj.someProp).to.equal('something');
});
But, with chai-as-promised, there’s an alternative way. We can make use of the fact you can chain promises:
var value = systemUnderTest().then(function(obj) {
return obj.someProp;
});

return expect(value).to.eventually.equal('something');
As the final alternative, if you are using ECMAScript 2015, you can make it a little bit cleaner using the fat arrow function syntax:
var value = systemUnderTest()

return expect(value.then(o => o.someProp)).to.eventually.equal('something');

Multiple Promises

If you have multiple promises in tests, you can use Promise.all similar to how you would use it in non-test code.
return Promise.all([
expect(value1).to.become('foo'),
expect(value2).to.become('bar')
]);
But keep in mind that this is similar to having multiple assertions in a single test, which can be seen as a code smell.

Comparing Multiple Promises

If you have two (or more) promises which you need to compare, then the following pattern can be used:
return Promise.all([p1, p2]).then(function(values) {
expect(values[0]).to.equal(values[1]);
});
In other words, we can use all to resolve both promises, and use a function in then to run a normal Chai assertion on the returned values.

Asserting for Failures

Occasionally you might want to check that a certain call makes a promise fail instead of succeed. In those cases, you can use chai-as-promised’s rejected assertion:
return expect(value).to.be.rejected;
If you want to ensure the rejection comes with a specific type of error or message, you can also use rejectedWith:
//require this promise to be rejected with a TypeError
return expect(value).to.be.rejectedWith(TypeError);

//require this promise to be rejected with message 'holy smokes, Batman!'
return expect(value).to.be.rejectedWith('holy smokes, Batman!');

Test Hooks

You can use promises in the test hooks in the same way as in any other test function. This works with before, after, beforeEach and afterEach. For example:
describe('something', function() {
before(function() {
return somethingThatReturnsAPromise();
});

beforeEach(function() {
return somethingElseWithPromises();
});
});
These work similar to how promises work in tests. If the promise is rejected, Mocha will throw an error.

Promises and Mocks/Stubs

Lastly, let’s look at how to use promises with stubs. I’m using Sinon.JS for the examples below. To do that, you need to install it by executing the command:
npm install sinon

Returning Promises from Stubs

If you need a stub or a mock to return a promise, the answer is fairly simple:
var stub = sinon.stub();

//return a failing promise
stub.returns(Promise.reject('a failure'));

//or a successful promise
stub.returns(Promise.resolve('a success'));

Spying on Promises

You can use spies as promise callbacks like other functions, but it might not be useful due to promises being asynchronous. If you need to do an assertion against a promise, you would be better off doing it using chai-as-promised.
var spy = sinon.spy();
var promise = systemUnderTest();

promise.then(spy);

Sinon-as-promised

To slightly simplify stubs and promises, we can use sinon-as-promised. It can be installed via npm:
npm install sinon-as-promised
It provides helper functions resolves and rejects on stubs
var sinon = require('sinon');

//this makes sinon-as-promised available in sinon:
require('sinon-as-promised');

var stub = sinon.stub();

//return a failing promise
stub.rejects('a failure');

//or a successful promise
stub.resolves('a success');

Conclusions

Promises can simplify our asynchronous code, and they can even simplify asynchronous tests – provided you add some helpful libraries to the mix. Mocha’s built-in promise support combined with Chai and chai-as-promised makes it simple to test promise-returning code. Add SinonJS and sinon-as-promised into the mix, and you can stub them easily too. One important thing to remember: when using promises in your tests, always return a promise from the test, otherwise Mocha won’t know of it, and your test may silently fail without telling you about it. As I mentioned in the introduction, I’ve created an example project that you can download from my website which shows the techniques introduced in this article. Feel free to download it and play with it.

Frequently Asked Questions (FAQs) about Promises in JavaScript Unit Tests

What is the significance of Promises in JavaScript unit tests?

Promises play a crucial role in JavaScript unit tests, especially when dealing with asynchronous operations. They provide a way to handle the eventual completion or failure of an asynchronous operation, and allow you to organize your code in a more readable and error-proof manner. Promises can be chained together to perform a series of asynchronous operations in a specific order. This makes your tests more reliable and easier to understand.

How can I handle errors in Promises while testing?

Error handling is an essential part of working with Promises in JavaScript. When a Promise is rejected, it’s usually due to an error occurring. You can handle these errors using the .catch() method. This method returns a Promise and deals with rejected cases only. It behaves the same as calling Promise.prototype.then(undefined, onRejected). In your tests, you can simulate errors and use .catch() to verify that the errors are handled correctly.

How can I test multiple Promises simultaneously?

JavaScript provides the Promise.all() method, which allows you to handle multiple Promises simultaneously. This method returns a single Promise that resolves when all of the Promises in the iterable argument have resolved, or rejects with the reason of the first passed Promise that rejects. This can be very useful in your tests when you need to perform multiple asynchronous operations in parallel and wait for all of them to complete.

How can I use Chai as Promised for testing Promises?

Chai as Promised extends Chai with a fluent language for asserting facts about Promises. It allows you to check if a Promise is fulfilled, rejected, or if it eventually has a certain value. To use it, you need to first install it and then add it to your Chai setup. Once it’s set up, you can use methods like .should.eventually.equal(value) to assert that a Promise will eventually equal a certain value.

How can I use Jest to test Promises?

Jest provides several methods to work with Promises, including .resolves and .rejects. These allow you to assert that a Promise resolves to a certain value or rejects with a certain error. Jest also provides a done callback that you can use to tell Jest that your test is complete. This can be useful when testing Promises, as you can call done in your Promise’s .then() or .catch() methods.

How can I avoid the ‘UnhandledPromiseRejectionWarning’ in my tests?

This warning is usually caused by a Promise that was rejected but did not have a corresponding .catch() handler. To avoid this warning, always attach a .catch() handler to your Promises, even if you’re not expecting them to reject. In your tests, you can also use a global error handler to catch any unhandled Promise rejections.

How can I test if a Promise has been fulfilled or rejected?

You can use the .then() method to attach callbacks that will be called when the Promise is fulfilled, or the .catch() method to attach callbacks that will be called when the Promise is rejected. In your tests, you can use these methods to assert that a Promise is fulfilled with a certain value or rejected with a certain error.

How can I make my Promise-based tests more readable?

One way to make your Promise-based tests more readable is by using async/await. This syntax allows you to write asynchronous code as if it were synchronous, making it easier to read and understand. You can use async/await in your tests to wait for Promises to resolve or reject before continuing with the test.

How can I test a Promise that should never resolve?

In some cases, you might want to test that a Promise never resolves. This can be tricky, as most testing frameworks will timeout if a test never completes. One approach is to use a done callback and never call it, causing the test to timeout if the Promise resolves. You can also use a .then() handler to throw an error if the Promise resolves.

How can I test the order of Promise resolution?

Testing the order of Promise resolution can be done by chaining Promises together using the .then() method. You can use this to assert that certain operations happen in a specific order. In your tests, you can use variables or data structures to keep track of the order in which operations are completed, and then assert that this order matches your expectations.

Jani HartikainenJani Hartikainen
View Author

Jani has built all kinds of JS apps for more than 15 years. At his blog, he helps JavaScript developers learn to eliminate bad code so they can focus on writing awesome apps and solve real problems.

AurelioDbrowser testingjavascriptperformance testingPromisesTestingunit testunit testingUnit Tests
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week