JavaScript
Article

Promises in JavaScript Unit Tests: the Definitive Guide

By Jani Hartikainen

Promises are becoming a common part of the JavaScript code. The native Promise object is already supported by all the major browsers including Chrome, Firefox, and Safari.

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.

Jani Hartikainen
Meet the 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.
  • Jocelyn Lecomte

    Nice article. I didn’t really get what you said in the ‘Asserting Against a Specific Property from an Object’ section. The first example, with vanilla chai is ok, but there is no assertion in the second example, when you use chai-as-promised…

    Also, I think there is a better way to assert on a specific property, not using then():

    return expect(obj).to.eventually.have.property(“someProp”).that.equals(‘something’);

    • Jani Hartikainen

      In the second example, `value` is a promise resolving to the property value, so you could assert against that.

      Also, that’s a great tip! I wasn’t aware that you can actually chain after the property like that. I think that’s definitely the most convenient way of doing this :) I checked it, and that works.

      • http://rawlinson.us/blog Bill

        I agree with Jocelyn that your example is a little unclear. I had to come to the comments to see what your intent was with that example.

        You went through the process of showing the assertions in every other example so it would probably make the article better if you filled that example to show the assertion instead of implying that you can assert on the returned value.

        Overall cool article though and I’m happy to know about the chai-as-promised library now.

        • Jani Hartikainen

          Thanks for the feedback Bill. I’ve sent a mail to the editor with an update to the article, so it should be fixed soon :)

          • http://careersreport.com Nida Berry

            There is a method how it is& possible to get sixty-five dollars an hour… After searching for a job that suits me for 6 months , I started earning over this web-site and now I could not be more happy . 3 months have passed since being on my new job and my income is around five thousand dollars/month -Check web-site i use on MY-DISQUS-PROFILE-PAGE

          • http://careersreport.com Dawnd Papp

            Here is how it is possible to get $fifty five hour… After searching for a job that suits me for six months , I started making money over this web-site and now I am very satisfied. After 3 months doing this my income is around $5000-per month -Check link on MY-_________-PROFILE_____ for more info

  • M S i N Lund

    Does this work in all browsers, even the crappy mobile ones people are stuck with?

    • Jani Hartikainen

      You can get good support by using a library such as es6-shim. If you can give me an example of which browsers you’re referring to, I can give you a better answer about their specific support :)

  • netaisllc

    Thanks for your efforts, Jani. This (finally) cleared up some fog blocking my progress.

    I found that, on occasion, using the .then() is helpful. Mostly when the resolution of the promise is a collection and I want to assert against several facets of it before passing the test.

    Also, in some cases, the resolution provides *data* to be used in later tests, such as when a server returns an _id of a just-posted asset. Grabbing in the .then() seems convenient.

  • codedungeon

    Really informative article, thank you very much neighbor (I am a Swede). Thanks for making this seem simple. I have to attack this very situation at work tomorrow, good timing to find this article

  • http://blog.willclark.tech willclarktech

    This is excellent and it’s so hard to find decent material on this. Thank you!

  • http://faustinelli.wordpress.com Muzietto

    Two minutes perusing your words and the solution of two cases I’d been struggling with the whole afternoon is found. Better than the official docs for Mocha and Chai-as-promised.
    This post will enter straight away my collection of useful links. Thank you!!!!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.