Promises in JavaScript Unit Tests: the Definitive Guide

    Jani Hartikainen
    Share

    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.