最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

node.js - JavaScriptMocha - How to test if function call was awaited - Stack Overflow

programmeradmin7浏览0评论

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?

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?

Share Improve this question edited Mar 7, 2019 at 9:33 Adam asked Mar 5, 2019 at 8:43 AdamAdam 5,2632 gold badges33 silver badges62 bronze badges 1
  • 1 By the way, if I take your ugly-async-test.js, save it here, and remove await from await this.methodB(), and then run the test, the test still passes. So the test you have there is not detecting a missing await. – Louis Commented Mar 7, 2019 at 15:08
Add a ment  | 

3 Answers 3

Reset to default 4 +100

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 plete
    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 pletion 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 pletes 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 pletes 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).

I had the same idea a while ago: Wouldn't it be nice to be able to detect asynchronous functions in a programmatic way? Turns out, you can't. At least you can't do this if you want to have reliable results.

The reason for this is pretty simple: async and await are basically syntactical sugar, provided by the piler. Let's look at how we wrote functions with promises, before those two new keywords existed:

function foo () {
  return new Promise((resolve, reject) => {
    // ...

    if (err) {
      return reject(err);
    }

    resolve(result);
  });
}

Something like that. Now this is cumbersome and annoying, and hence marking a function as async allows to write this simpler, and let the piler add the new Promise wrapper:

async function foo () {
  // ...

  if (err) {
    throw err;
  }

  return result;
}

Although we can now use throw and return, what's happening under the hood is exactly the same as before: The piler adds a return new Promise wrapper and for each return, it calls resolve, for each throw it calls reject.

You can easily see that this is actually the same as before, as you can define a function with async, but then call if from the outside without await, by using the good old then syntax of promises:

foo().then(...);

The same is true the other way round: If a function is defined using the new Promise wrapper, you can await it. So, to cut a long story short, async and await are just neat syntaxes to do something you otherwise would need to do manually.

And this in turn means that even if you define a function with async, there is absolutely no guarantee that it has actually been called with await! And if the await is missing, this not necessarily means that it is an error – maybe someone just prefers the then syntax.

So, to summarize, even if there is a technical solution for your question, it won't help, at least not in all cases, because you do not need to call an async function with await without sacrificing being asynchronous.

I understand that in your scenario you would like to make sure that the promise was actually awaited, but IMHO you then spend a lot of time to build a solution that is plex, but doesn't catch every problem that might be there. So, from my very personal point of view, it's not worth the effort.

Terminological note: what you're essentially asking is to detect "floating promises". This contains code that creates a floating promise:

methodA() {
   this.methodB()
       .then(() => true); // .then() returns a promise that is lost
},

This too:

async methodA() {
   // The promise returned by this.methodB() is not chained with the one
   // returned by methodA.
   this.methodB();
   return true; 
},

In the first case you'd add return to allow the caller to chain the promise. In the second case you'd use await to chain the promise returned by this.methodB() to the promise returned by methodA.

One thing that plicates the aim of dealing with floating promises is that sometimes developers have good reasons to let a promise be floating. So any detection method needs to provide a way to say "this floating promise is okay".

There are a few approaches you could use.

Use Type Analysis

If you use tools that provide static type checking, you can catch floating promises prior to running the code.

I know you can definitely do it with TypeScript used in conjunction with tslint because I have experience with these. The TypeScript piler provides the type information, and if you set tslint to run the no-floating-promises rule, then tslint will use the type information to detect the floating promises in the two cases above.

The TypeScript piler can do type analysis on plain JS files so in theory your code base could remain the same and you'd just need to configure the TypeScript piler with a configuration like this:

{
  "pilerOptions": {
    "allowJs": true, // To allow JS files to be fed to the piler.
    "checkJs": true, // To actually turn on type-checking.
    "lib": ["es6"] // You need this to get the declaration of the Promise constructor.
  },
  "include": [
    "*.js", // By default JS files are not included.
    "*.ts" // Since we provide an explicit "include", we need to state this too.
  ]
}

The paths in "include" would need to be adapted to your specific project layout. You'd need something like this for tslint.json:

{
  "jsRules": {
    "no-floating-promises": true
  }
}

I wrote in theory above, because as we speak tslint cannot use type information on JavaScript files, even if allowJs and checkJs are true. As it so happens, there's a tslint issue about this problem, submitted by someone who (coincidence!) happened to want to run the no-floating-promise rule on plain JS files.

So as we speak, in order to be able to benefit from the check above, you'd have to make your codebase TypeScript.

In my experience, once you have a TypeScript and tslint setup running this will detect all floating promises in your code, and won't report spurious cases. Even if you have a promise you want to leave floating in your code you can use a tslint directive like // tslint:disable-next-line:no-floating-promises. And it does not matter if third-party libraries willfully let promises floating: you configure tslint to report only issues with your code so it won't report those that exist in third-party libraries.

There are other systems that provide type analysis, but I'm not familiar with them. For instance, Flow might also work but I've never used it so I cannot say whether it would work.

Use a Promise Library That Detects Floating Promises at Runtime

This approach is not as reliable as type analysis for detecting problems in your code while ignoring problems elsewhere.

The problem is that I don't know of a promise library that will generally, reliably, and simultaneously meet these two requirements:

  1. Detect all cases of floating promises.

  2. Not report cases you don't care about. (In particular, floating promises in third-party code.)

In my experience configuring a promise library to improve how it handles one of the two requirements harms how it handles the other requirement.

The promise library I'm most familiar with is Bluebird. I was able to detect floating promises with Bluebird. However, while you can mix Bluebird promises with any promises produced by a framework that follows Promises/A+, when you do this kind of mixing, you prevent Bluebird from detecting some floating promises. You can improve the chances of detecting all cases by replacing the default Promise implementation with Bluebird but

  1. Libraries that explicitly use a 3rd-party implementation rather than the native one (e.g. const Promise = require("some-spiffy-lib")) will still use that implementation. So you may not be able to get all the code running during your test to use Bluebird.

  2. And you may end up getting spurious warnings about floating promises that are willfully left floating in third-party libraries. (Remember, sometimes developers leave promises floating on purpose.) Bluebird does not know which is your code and which isn't. It will report all cases it is able to detect. In your own code, you can indicate to Bluebird that you want to leave the promise floating, but in third-party code, you'd have to go modify that code to silence the warning.

Because of these issues, I would not use this approach for rigorous detection of floating promises.

发布评论

评论列表(0)

  1. 暂无评论