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

时间:2019-03-05 08:43:02

标签: javascript node.js testing mocha sinon

我想编写一个测试来检查我的函数是否使用await关键字调用其他函数。

我希望我的测试失败

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

我希望测试成功

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

我也希望测试也成功

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

我有一个解决方案,方法是使用process.nextTick对该方法进行存根并强制其返回假诺言,但这似乎很丑陋,我不想使用process.nextTick或{{1 }}等测试。

ugly-async-test.js

setTimeout

测试函数调用中是否使用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); }); }); 的聪明方法是什么?

3 个答案:

答案 0 :(得分:0)

前段时间我有相同的想法:能够以编程方式检测异步函数会不会很好?原来,你不能。如果想获得可靠的结果,至少不能这样做。

其原因非常简单:asyncawait基本上是语法糖,由编译器提供。让我们看看在这两个新关键字存在之前,我们如何用promise编写函数:

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

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

    resolve(result);
  });
}

类似的东西。现在这很麻烦且烦人,因此将函数标记为async可以使编写起来更简单,并让编译器添加new Promise包装器:

async function foo () {
  // ...

  if (err) {
    throw err;
  }

  return result;
}

尽管我们现在可以使用throwreturn,但实际情况与之前完全相同:编译器添加了一个return new Promise包装器,并为每个return添加了一个包装器。 ,它将调用resolve,对于每个调用throw的{​​{1}}。

您可以轻松地看到它实际上与以前相同,因为您可以使用reject定义一个函数,但是如果从外部 without async调用,通过使用承诺的良好旧await语法:

then

反之亦然:如果使用foo().then(...); 包装器定义了函数,则可以new Promise对其进行包装。因此,简而言之,awaitasync只是完成某些操作所需的巧妙语法。

这反过来意味着即使使用await定义一个函数,也绝对没有{em> guarantee 实际上已经用async调用了!而且,如果缺少await,则不一定意味着它是一个错误-也许有人只是更喜欢await语法。

总而言之,即使您的问题有一个技术解决方案,它也无济于事,至少在所有情况下都无济于事,因为您无需调用{{1 }}与then一起使用,而不会牺牲异步性。

我了解,在您的情况下,您想确保已真正兑现了诺言,但是恕我直言,您随后花了很多时间来构建一个复杂的解决方案,但并未解决可能存在的所有问题。因此,从我个人的角度来看,这是不值得的。

答案 1 :(得分:0)

TLDR

如果methodAawait上调用methodB,那么{em}由Promise返回的methodA将直到解析由Promise返回methodB解决。

另一方面,如果methodA没有在await上调用methodB,则 Promise返回的methodA将立即解决Promise返回的methodB已解决

因此,测试methodA是否在await上调用methodB只是测试Promise返回的methodA是否等待{{1}由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 }); }); methodA都返回一个methodB

我将把Promise返回的Promise称为methodA,将promiseA返回的Promise称为methodB

您要测试的是promiseB等待解决直到promiseA解决。


首先,让我们看一下如何测试promiseB不等待promiseA


测试promiseB是否不等待promiseA

测试否定情况(promiseB不等待promiseA)的一种简单方法是模拟promiseB返回一个{strong>从不解决:

methodB

这是一个非常干净,简单且直接的测试。


如果我们可以返回相反的结果,那就太棒了……如果测试失败,返回 true

不幸的是,这不是一个合理的方法,因为如果Promisedescribe('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(); }); }); promiseA该测试就会超时

我们将需要一种不同的方法。


背景信息

在继续之前,这里有一些有用的背景信息:

JavaScript使用message queue。下一条开始之前的当前消息runs to completion正在运行测试时,该测试是当前消息

ES6引入了PromiseJobs queue来处理“响应承诺的解决”的工作。 PromiseJobs队列中的所有作业都在当前消息完成之后且下一条消息开始之前运行

因此await解析时,其promiseB回调将添加到PromiseJobs队列中,并且当当前消息完成时,PromiseJobs中的所有作业将在订购,直到队列为空。

Promisethen只是syntactic sugar over promises and generators。在async上调用await实际上将其余函数包装在待解决的await解析后在PromiseJobs中安排的回调中。


我们需要的是一项测试,如果Promise DID等待Promise,它会告诉我们,而不会超时。

由于我们不希望测试超时,因此promiseApromiseB 都必须解决。

然后,目标是找出一种方法来判断promiseA是否正在等待promiseB ,因为它们都在解决

答案是利用PromiseJobs队列。

考虑此测试:

promiseA

promiseB 返回已解决的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 }); ,因此这两个回调将立即添加到PromiseJobs队列中。一旦当前消息(测试)暂停以等待PromiseJobs中的作业,它们将按照添加到PromiseJobs队列中的顺序运行,并且当测试Promise.resolve()之后继续运行时,Promise数组包含await Promise.all符合预期。

现在考虑进行此测试:

order

在这种情况下,我们保存了第一个['1', '2']中的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 }); ,以便稍后使用。 由于第一个resolve尚未解决,因此Promise回调不会立即添加到PromiseJobs队列中。另一方面,第二个Promise已解决,因此其then回调已添加到PromiseJobs队列中。一旦发生这种情况,我们将调用保存的Promise,以便解析第一个then,这会将其resolve回调添加到PromiseJobs队列的末尾。将当前消息(测试)暂停以等待PromiseJobs中的作业后,Promise数组将按预期包含then


  

测试函数调用中是否使用order的聪明方法是什么?

测试函数调用中是否使用['2', '1']的明智方法是向awaitawait都添加一个then回调,然后延迟解决promiseA 。如果promiseB等待promiseB,则其回调将始终是PromiseJobs队列中的最后一个。另一方面,如果promiseA不等待promiseB,则其回调将在PromiseJobs中排在第一队列中。

最终解决方案位于 TLDR 部分中。

请注意,当promiseA是在promiseB上调用methodA的{​​{1}}函数时,以及async是普通(不是await的函数,该函数返回链接到methodB返回的methodA的{​​{1}}(这是可以预期的,因为async只是语法糖Promise和生成器)。

答案 2 :(得分:-1)

术语注释:您实质上要问的是检测“浮动承诺”。这包含创建浮动承诺的代码:

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

这也是:

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

在第一种情况下,您将添加return以允许呼叫者链接承诺。在第二种情况下,您将使用awaitthis.methodB()返回的承诺链接到methodA返回的承诺。

使处理浮动承诺的目标变得复杂的一件事是,有时开发人员有充分的理由使承诺浮动。因此,任何检测方法都需要提供一种方式来表示“此浮动承诺还可以”。

您可以使用几种方法。

使用类型分析

如果您使用提供静态类型检查的工具,则可以在运行代码之前捕获浮动承诺。

我知道您肯定可以将TypeScript与tslint一起使用,因为我对此有经验。 TypeScript编译器提供类型信息,如果您将tslint设置为运行no-floating-promises规则,则tslint将使用类型信息来检测上述两种情况下的浮动承诺。 / p>

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.
  ]
}

"include"中的路径需要根据您的特定项目布局进行调整。您tslint.json需要这样的东西:

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

我在上面在理论上写了 ,因为我们所说的 tslint不能在JavaScript文件上使用类型信息,即使allowJs和{{ 1}}是对的。碰巧的是,有一个a tslint issue关于此问题,由碰巧要在普通JS文件上运行checkJs规则的某人提交。

正如我们所说的,为了能够从上面的检查中受益,您必须制作代码库TypeScript。

根据我的经验,一旦您运行了TypeScript和no-floating-promise安装程序,它将在代码中检测到所有浮动承诺,并且不会报告虚假情况。即使您承诺要在代码中保持浮动,也可以使用tslint之类的tslint指令。第三方库是否故意让诺言浮动也没有关系:您将// tslint:disable-next-line:no-floating-promises配置为仅报告代码问题,因此不会报告第三方库中存在的问题。

还有其他提供类型分析的系统,但我对它们不熟悉。例如,Flow可能也可以工作,但是我从未使用过它,所以我不能说它是否会工作。

使用在运行时检测浮动承诺的Promise库

这种方法不像类型分析那样可靠,它可以在忽略其他地方的问题时检测您的代码中的问题。

问题是我不知道一个会普遍,可靠且同时满足以下两个要求的诺言库:

  1. 检测所有浮动承诺的情况。

  2. 不报告您不关心的案例。 (尤其是第三方代码中的浮动承诺。)

根据我的经验,配置一个Promise库来改善它如何处理两个需求之一会损害它如何处理另一个需求。

我最熟悉的Promise库是Bluebird。我能够通过Bluebird检测到浮动承诺。但是,尽管可以将Bluebird承诺与Promises / A +之后的框架产生的任何承诺混合在一起,但是当您进行这种混合时,可以防止Bluebird检测到 some 浮动承诺。您可以通过将默认的tslint实现替换为Bluebird但

来提高发现所有案例的机会
  1. 显式使用第三方实现而不是本机实现(例如Promise)的库仍将使用该实现。因此,在使用Bluebird的测试过程中,您可能无法使所有代码都运行。

  2. 您可能最终会收到关于第三方库中被故意浮动的浮动承诺的虚假警告。 (请记住,有时开发人员会故意放下承诺 。)Bluebird不知道哪个是您的代码,哪个不是。它将报告能够检测到的所有情况。在您自己的代码中,您可以向Bluebird表示您要使承诺保持浮动,但是在第三方代码中,您必须去修改该代码以使警告消失。

由于这些问题,我不会将这种方法用于严格检测浮动承诺。