Unit Testing JavaScript Promises with Synchronous Tests
By Adrian Sutton
With Promise/A+ spreading through the world of JavaScript at a rapid pace, there’s one little detail that makes them very hard to unit test: any chained actions (via .then()) are only called when the execution stack contains only platform code. That’s a good thing in the real world but it makes unit testing much more complex because resolving a promise isn’t enough – you also need to empty the execution stack.
The first, and best, way to address these challenges is to take advantage of your test runner’s asynchronous test support. Mocha for example allows you to return a promise from your test and waits for it to be either resolved to indicate success or rejected to indicate failure. For example:
it('should pass asynchronously', function() { return new Promise((resolve, reject) => { setTimeout(resolve, 100); }) });
This works well when you’re testing code that returns the promise to you so you can chain any assertions you need and then return the final promise to the test runner. However, there are often cases where promises are used internally to a component which this approach can’t solve. For example, a UI component that periodically makes requests to the server to update the data it displays.
Sinon.js makes it easy to stub out the HTTP request using it’s fake server and the periodic updates using a fake clock, but if promises are used sinon’s clock.tick() isn’t enough to trigger chained actions. They’ll only execute after your test method returns and since there’s no reason, and often no way, for the UI component to pass a promise for it’s updates out of the component we can’t just depend on the test runner. That’s where promise-mock comes in. It replaces the normal Promise implementation with one that allows your unit test to trigger callbacks at any point.
Let’s avoid all the clock and HTTP stubbing by testing this very simple example of code using a Promise internally:
let value = ; module.exports = { setValueViaImmediatePromise: function (newValue) { return new Promise((resolve, reject) => resolve(newValue)) .then(result => value = result); },getValue<span style="color: #333333;">:</span> <span style="color: #008800; font-weight: bold;">function</span> () { <span style="color: #008800; font-weight: bold;">return</span> value; }
};
Our test is then:
const asyncThing = require('./asyncThing'); const PromiseMock = require('promise-mock'); const expect = require('expect.js'); describe.only('with promise-mock', function() { beforeEach(function() { PromiseMock.install(); });afterEach(<span style="color: #008800; font-weight: bold;">function</span>() { PromiseMock.uninstall(); }); it(<span style="background-color: #fff0f0;">'should set value asynchronously and keep internals to itself'</span>, <span style="color: #008800; font-weight: bold;">function</span>() { asyncThing.setValueViaImmediatePromise(<span style="color: #0000dd; font-weight: bold;">3</span>); Promise.runAll(); expect(asyncThing.getValue()).to.be(<span style="color: #0000dd; font-weight: bold;">3</span>); });
});
We have a beforeEach and afterEach to install and uninstall the mocked promise, then when we want the promise callbacks to execute in our test, we simply call Promise.runAll(). In most cases, promise-mock combined with sinon’s fake HTTP server and stub clock is enough to let us write easy-to-follow, synchronous tests that cover asynchronous behaviour.
Keeping our tests synchronous isn’t just about making them easy to read though – it also means we’re in control of how asynchronous callbacks interleave. So we can write tests to check what happens if action A finishes before action B and tests for what happens if it’s the other way around. Lots and lots of bugs hide in those areas.
PromiseMock.install() Not Working
All that sounds great, but I spent a long time trying to work out why PromiseMock.install() didn’t ever seem to change the Promise implementation. I could see that window.Promise === PromiseMock was true, but without the window prefix I was still getting the original promise implementation (Promise !== PromiseMock).
It turns out, that’s because we were using babel’s transform-runtime plugin which was very helpfully rewriting references to Promise to use babel’s polyfill version without the polyfill needing to pollute the global namespace. The transform-runtime plugin has an option to disable this:
['transform-runtime', {polyfill: false}]
With that promise-mock worked as expected.