JavaScript / Mocha - How to test if function call was awaited
TLDR
If methodA
calls await
on methodB
then the Promise
returned by methodA
will not resolve until the Promise
returned by methodB
resolves.
On the other hand, if methodA
does not call await
on methodB
then the Promise
returned by methodA
will resolve immediately whether the Promise
returned by methodB
has resolved or not.
So testing if methodA
calls await
on methodB
is just a matter of testing whether the Promise
returned by methodA
waits for the Promise
returned by methodB
to resolve before it resolves:
const { stub } = require('sinon');
const { expect } = require('chai');
const testObject = {
async methodA() {
await this.methodB();
},
async methodB() { }
};
describe('methodA', () => {
const order = [];
let promiseB;
let savedResolve;
beforeEach(() => {
promiseB = new Promise(resolve => {
savedResolve = resolve; // save resolve so we can call it later
}).then(() => { order.push('B') })
stub(testObject, 'methodB').returns(promiseB);
});
afterEach(() => {
testObject.methodB.restore();
});
it('should await methodB', async () => {
const promiseA = testObject.methodA().then(() => order.push('A'));
savedResolve(); // now resolve promiseB
await Promise.all([promiseA, promiseB]); // wait for the callbacks in PromiseJobs to complete
expect(order).to.eql(['B', 'A']); // SUCCESS: 'B' is first ONLY if promiseA waits for promiseB
});
});
Details
In all three of your code examples methodA
and methodB
both return a Promise
.
I will refer to the Promise
returned by methodA
as promiseA
, and the Promise
returned by methodB
as promiseB
.
What you are testing is if promiseA
waits to resolve until promiseB
resolves.
First off, let's look at how to test that promiseA
did NOT wait for promiseB
.
Test if promiseA
does NOT wait for promiseB
An easy way to test for the negative case (that promiseA
did NOT wait for promiseB
) is to mock methodB
to return a Promise
that never resolves:
describe('methodA', () => {
beforeEach(() => {
// stub methodB to return a Promise that never resolves
stub(testObject, 'methodB').returns(new Promise(() => {}));
});
afterEach(() => {
testObject.methodB.restore();
});
it('should NOT await methodB', async () => {
// passes if promiseA did NOT wait for promiseB
// times out and fails if promiseA waits for promiseB
await testObject.methodA();
});
});
This is a very clean, simple, and straightforward test.
It would be awesome if we could just return the opposite...return true if this test would fail.
Unfortunately, that is not a reasonable approach since this test times out if promiseA
DOES await
promiseB
.
We will need a different approach.
Background Information
Before continuing, here is some helpful background information:
JavaScript uses a message queue. The current message runs to completion before the next one starts. While a test is running, the test is the current message.
ES6 introduced the PromiseJobs queue which handles jobs "that are responses to the settlement of a Promise". Any jobs in the PromiseJobs queue run after the current message completes and before the next message begins.
So when a Promise
resolves, its then
callback gets added to the PromiseJobs queue, and when the current message completes any jobs in PromiseJobs will run in order until the queue is empty.
async
and await
are just syntactic sugar over promises and generators. Calling await
on a Promise
essentially wraps the rest of the function in a callback to be scheduled in PromiseJobs when the awaited Promise
resolves.
What we need is a test that will tell us, without timing out, if promiseA
DID wait for promiseB
.
Since we don't want the test to timeout, both promiseA
and promiseB
must resolve.
The objective, then, is to figure out a way to tell if promiseA
waited for promiseB
as they are both resolving.
The answer is to make use of the PromiseJobs queue.
Consider this test:
it('should result in [1, 2]', async () => {
const order = [];
const promise1 = Promise.resolve().then(() => order.push('1'));
const promise2 = Promise.resolve().then(() => order.push('2'));
expect(order).to.eql([]); // SUCCESS: callbacks are still queued in PromiseJobs
await Promise.all([promise1, promise2]); // let the callbacks run
expect(order).to.eql(['1', '2']); // SUCCESS
});
Promise.resolve()
returns a resolved Promise
so the two callbacks get added to the PromiseJobs queue immediately. Once the current message (the test) is paused to wait for the jobs in PromiseJobs, they run in the order they were added to the PromiseJobs queue and when the test continues running after await Promise.all
the order
array contains ['1', '2']
as expected.
Now consider this test:
it('should result in [2, 1]', async () => {
const order = [];
let savedResolve;
const promise1 = new Promise((resolve) => {
savedResolve = resolve; // save resolve so we can call it later
}).then(() => order.push('1'));
const promise2 = Promise.resolve().then(() => order.push('2'));
expect(order).to.eql([]); // SUCCESS
savedResolve(); // NOW resolve the first Promise
await Promise.all([promise1, promise2]); // let the callbacks run
expect(order).to.eql(['2', '1']); // SUCCESS
});
In this case we save the resolve
from the first Promise
so we can call it later. Since the first Promise
has not yet resolved, the then
callback does not immediately get added to the PromiseJobs queue. On the other hand, the second Promise
has already resolved so its then
callback gets added to the PromiseJobs queue. Once that happens, we call the saved resolve
so the first Promise
resolves, which adds its then
callback to the end of the PromiseJobs queue. Once the current message (the test) is paused to wait for the jobs in PromiseJobs, the order
array contains ['2', '1']
as expected.
What is the smart way to test if
await
was used in the function call?
The smart way to test if await
was used in the function call is to add a then
callback to both promiseA
and promiseB
, and then delay resolving promiseB
. If promiseA
waits for promiseB
then its callback will always be last in the PromiseJobs queue. On the other hand, if promiseA
does NOT wait for promiseB
then its callback will get queued first in PromiseJobs.
The final solution is above in the TLDR section.
Note that this approach works both when methodA
is an async
function that calls await
on methodB
, as well as when methodA
is a normal (not async
) function that returns a Promise
chained to the Promise
returned by methodB
(as would be expected since, once again, async / await
is just syntactic sugar over Promises
and generators).