简体   繁体   English

JavaScript / Mocha - 如何测试函数调用是否等待

[英]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. 我想编写一个测试,检查我的函数是否使用await关键字调用其他函数。

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. 我有一个解决方法,通过stubing方法并强制它使用process.nextTick返回其中的假承诺,但它似乎很丑,我不想在我的测试中使用process.nextTicksetTimeout等。

ugly-async-test.js 丑异步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? 测试await是否在函数调用中使用的智能方法是什么?

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 compiler. 原因很简单: asyncawait基本上都是语法糖,由编译器提供。 Let's look at how we wrote functions with promises, before those two new keywords existed: 让我们看看在存在这两个新关键字之前,我们如何使用promises编写函数:

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 compiler add the new Promise wrapper: 现在这很麻烦,很烦人,因此标记一个函数async允许写这个更简单,并让编译器添加new Promise包装器:

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 compiler adds a return new Promise wrapper and for each return , it calls resolve , for each throw it calls reject . 虽然我们现在可以使用throwreturn ,但是在幕后发生的事情与之前完全相同:编译器添加了一个return new Promise包装器,对于每个return ,它调用resolve ,对于它调用的每个throwreject

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: 你可以很容易地看到,这实际上是和以前一样,你可以定义一个函数async ,但随后从外面打电话,如果没有 await ,使用好老then承诺的语法:

foo().then(...);

The same is true the other way round: If a function is defined using the new Promise wrapper, you can await it. 反过来也是如此:如果使用new Promise包装器定义了一个函数,则可以await它。 So, to cut a long story short, async and await are just neat syntaxes to do something you otherwise would need to do manually. 因此,简而言之, asyncawait只是简洁的语法来做你需要手动完成的事情。

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 ! 而这反过来意味着即使你用async定义一个函数,也绝对不能保证它实际上是用await调用的! And if the await is missing, this not necessarily means that it is an error – maybe someone just prefers the then syntax. 如果缺少await ,这并不一定意味着它是一个错误 - 也许有人只是喜欢then语法。

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. 因此,总而言之,即使有一个针对您的问题的技术解决方案,它也无济于事,至少在所有情况下都不会如此,因为您不需要await不牺牲异步的情况下调用async函数。

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 complex, but doesn't catch every problem that might be there. 我知道在您的场景中,您希望确保实际等待承诺,但是恕我直言,然后您将花费大量时间来构建一个复杂的解决方案,但不能解决可能存在的所有问题。 So, from my very personal point of view, it's not worth the effort. 所以,从我个人的角度来看,这不值得付出努力。

TLDR TLDR

If methodA calls await on methodB then the Promise returned by methodA will not resolve until the Promise returned by methodB resolves . 如果methodA呼叫awaitmethodB Promise通过返回methodA不会解决,直到Promise通过返回methodB解决

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 . 在另一方面,如果methodA不叫awaitmethodB Promise通过返回methodA将立即解决是否Promise通过返回methodB已经解决或没有

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: 如果是这样的测试methodA呼叫awaitmethodB仅仅是一个测试的事是否Promise通过返回methodA等待Promise返回通过methodB解决它结算前:

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 . 在所有这三个你的代码示例methodAmethodB都返回一个Promise

I will refer to the Promise returned by methodA as promiseA , and the Promise returned by methodB as promiseB . 我指的是Promise通过返回methodApromiseA ,并Promise通过返回methodBpromiseB

What you are testing is if promiseA waits to resolve until promiseB resolves. 您正在测试的是promiseA等待解决,直到promiseB解决。


First off, let's look at how to test that promiseA did NOT wait for promiseB . 首先,让我们来看看如何测试promiseA不等待promiseB


Test if promiseA does NOT wait for promiseB 测试promiseA是否等待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 : 测试负面情况( promiseA不等待promiseB )的简单方法是模拟methodB以返回永不解析Promise

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 . 如果我们可以返回相反的情况会很棒...如果此测试失败则返回true

Unfortunately, that is not a reasonable approach since this test times out if promiseA DOES await promiseB . 不幸的是,这不是一个合理的方法,因为如果promiseA await promiseB 这个测试会超时

We will need a different approach. 我们需要一种不同的方法。


Background Information 背景资料

Before continuing, here is some helpful background information: 在继续之前,这里有一些有用的背景信息:

JavaScript uses a message queue . JavaScript使用消息队列 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". ES6引入了PromiseJobs队列 ,该队列处理“对Promise结算的响应”。 Any jobs in the PromiseJobs queue run after the current message completes and before the next message begins . PromiseJobs队列中的所有作业在当前消息完成之后和下一条消息开始之前运行。

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. 因此,当一个Promise解决,其then的回调被添加到PromiseJobs队列 ,对当前消息完成时PromiseJobs所有作业都将在顺序运行,直到队列为空。

async and await are just syntactic sugar over promises and generators . asyncawait只是承诺和生成器的语法糖 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. 当等待的Promise结算时,在Promise上调用await实际上将函数的其余部分包含在PromiseJobs中的调用中。


What we need is a test that will tell us, without timing out, if promiseA DID wait for promiseB . 我们需要的是一个测试,告诉我们,如果promiseA DID等待promiseB ,没有超时。

Since we don't want the test to timeout, both promiseA and promiseB must resolve. 由于我们不希望测试超时,因此promiseApromiseB 必须解决。

The objective, then, is to figure out a way to tell if promiseA waited for promiseB as they are both resolving . 那么,目标是找出一种方法来判断promiseA等待promiseB 因为它们都在解决

The answer is to make use of the PromiseJobs queue. 答案是使用PromiseJobs队列。

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. Promise.resolve() 返回一个已解析的Promise因此两个回调会立即添加到PromiseJobs队列中。 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. 一旦当前消息(测试)暂停以等待PromiseJobs中的作业,它们按照它们被添加到PromiseJobs队列的顺序运行,并且当await Promise.all之后测试继续运行时, order数组包含['1', '2']如预期的那样。

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. 在这种情况下,我们保存第一个Promiseresolve ,以便我们稍后调用它。 Since the first Promise has not yet resolved, the then callback does not immediately get added to the PromiseJobs queue . 因为第一Promise仍未解决, then回调并不会立即被添加到队列PromiseJobs。 On the other hand, the second Promise has already resolved so its then callback gets added to the PromiseJobs queue. 在另一方面,第二Promise已经解决,以便其then的回调被添加到队列PromiseJobs。 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. 一旦出现这种情况,我们称之为保存的resolve所以第一Promise解决,这增加了它then回调到PromiseJobs队列的末尾。 Once the current message (the test) is paused to wait for the jobs in PromiseJobs, the order array contains ['2', '1'] as expected. 暂停当前​​消息(测试)以等待PromiseJobs中的作业后, order数组按预期包含['2', '1']


What is the smart way to test if await was used in the function call? 测试await是否在函数调用中使用的智能方法是什么?

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 . 测试await是否在函数调用中使用的智能方法是向promiseApromiseB添加一个then回调,然后延迟解析promiseB If promiseA waits for promiseB then its callback will always be last in the PromiseJobs queue. 如果promiseA等待promiseB那么它的回调将永远是 PromiseJobs队列中的最后一个 On the other hand, if promiseA does NOT wait for promiseB then its callback will get queued first in PromiseJobs. 另一方面,如果promiseA不等待promiseB那么它的回调将首先在PromiseJobs中排队。

The final solution is above in the TLDR section. 最终解决方案在TLDR部分中。

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). 注意,这种方法的工作原理都当methodA是一个async调用函数awaitmethodB ,以及何时methodA是正常的(不是async )函数返回一个Promise拴在Promise通过返回methodB (如将因为可以预期,一旦再一次, async / await只是Promises和generator上的语法糖。

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. 在第一种情况下,您需要添加return以允许调用者链接promise。 In the second case you'd use await to chain the promise returned by this.methodB() to the promise returned by methodA . 在第二种情况下,您将使用awaitthis.methodB()返回的promise链接到methodA返回的promise。

One thing that complicates 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. 如果使用提供静态类型检查的工具,则可以在运行代码之前捕获浮动的promise。

I know you can definitely do it with TypeScript used in conjunction with tslint because I have experience with these. 我知道你肯定可以使用与tslint结合使用的TypeScript,因为我有这方面的经验。 The TypeScript compiler 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. TypeScript编译器提供类型信息,如果您设置tslint以运行no-floating-promises规则,则tslint将使用类型信息来检测上述两种情况中的浮动promise。

The TypeScript compiler 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 compiler with a configuration like this: TypeScript编译器可以对普通JS文件进行类型分析,因此从理论上讲,您的代码库可以保持不变,您只需要使用如下配置来配置TypeScript编译器:

{
  "compilerOptions": {
    "allowJs": true, // To allow JS files to be fed to the compiler.
    "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. "include"的路径需要根据您的特定项目布局进行调整。 You'd need something like this for tslint.json : 对于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. 上面的理论中写道,因为正如我们所说, tslint不能在JavaScript文件上使用类型信息,即使allowJscheckJs为真。 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. 正如它所发生的那样,有一个关于这个问题tslint问题,由某人提出(巧合!)碰巧想要在普通的JS文件上运行no-floating-promise规则。

So as we speak , in order to be able to benefit from the check above, you'd have to make your codebase TypeScript. 因此,在我们发言时 ,为了能够从上面的检查中受益,您必须制作代码库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. 根据我的经验,一旦你运行了TypeScript和tslint设置,这将检测代码中的所有浮动promise,并且不会报告虚假案例。 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 . 即使您有一个承诺,您希望在代码中保留浮动,您也可以使用tslint指令,例如// 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. 并且,如果第三方库故意让promises浮动,则tslint :您将tslint配置为仅报告代码问题,以便它不会报告第三方库中存在的问题。

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. 例如,Flow也可以工作,但我从未使用它,所以我不能说它是否会起作用。

Use a Promise Library That Detects Floating Promises at Runtime 使用在运行时检测浮动承诺的Promise库

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: 问题是我不知道一个通​​常,可靠,同时满足这两个要求的promise库:

  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. 根据我的经验,配置promise库以改进它处理这两个要求之一的方式会损害它如何处理其他要求。

The promise library I'm most familiar with is Bluebird. 我最熟悉的诺言库是Bluebird。 I was able to detect floating promises with Bluebird. 我能够检测到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. 然而,虽然你可以将Bluebird的承诺与遵循Promises / A +的框架产生的任何承诺混合,但是当你进行这种混合时,你会阻止Bluebird检测到一些浮动的承诺。 You can improve the chances of detecting all cases by replacing the default Promise implementation with Bluebird but 您可以通过使用Bluebird替换默认的Promise实现来提高检测所有案例的几率

  1. Libraries that explicitly use a 3rd-party implementation rather than the native one (eg const Promise = require("some-spiffy-lib") ) will still use that implementation. 明确使用第三方实现而不是本机实现的库(例如const Promise = require("some-spiffy-lib") )仍将使用该实现。 So you may not be able to get all the code running during your test to use Bluebird. 因此,您可能无法在测试期间运行所有代码以使用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. (请记住,有时开发人员会故意放弃承诺。)Bluebird不知道哪些是您的代码,哪些不是。 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. 在您自己的代码中,您可以向Bluebird表明您希望保留浮动,但在第三方代码中,您必须修改该代码以使警告静音。

Because of these issues, I would not use this approach for rigorous detection of floating promises. 由于这些问题,我不会使用这种方法来严格检测浮动承诺。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM