Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
783 views
in Technique[技术] by (71.8m points)

node.js - JavaScript / Mocha - How to test if function call was awaited

I would like to write a test that check if my function calls other functions using the await keyword.

I'd like my test to fail:

async methodA() {
   this.methodB();
   return true; 
},

I'd like my test to succeed:

async methodA() {
   await this.methodB();
   return true;
},

I'd like my test to succeed too:

methodA() {
   return this.methodB()
       .then(() => true);
},

I have a solution by stubbing the method and force it to return fake promise inside it using process.nextTick, but it seems to be ugly, and I do not want to use process.nextTick nor setTimeout etc in my tests.

ugly-async-test.js

const { stub } = require('sinon');
const { expect } = require('chai');

const testObject = {
    async methodA() {
        await this.methodB();
    },
    async methodB() {
        // some async code
    },
};

describe('methodA', () => {
    let asyncCheckMethodB;

    beforeEach(() => {
        asyncCheckMethodB = stub();
        stub(testObject, 'methodB').returns(new Promise(resolve => process.nextTick(resolve)).then(asyncCheckMethodB));
    });

    afterEach(() => {
        testObject.methodB.restore();
    });

    it('should await methodB', async () => {
        await testObject.methodA();
        expect(asyncCheckMethodB.callCount).to.be.equal(1);
    });
});

What is the smart way to test if await was used in the function call?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...