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

问题描述 投票:7回答:3

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

我希望我的测试失败:

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

我希望我的测试成功:

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

我也希望我的测试成功:

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

我有一个解决方法,通过存根方法并强制它使用process.nextTick返回其中的假承诺,但它似乎很丑,我不想在我的测试中使用process.nextTicksetTimeout等。

丑异步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);
    });
});

如果在函数调用中使用await,测试的智能方法是什么?

javascript node.js testing mocha sinon
3个回答
0
投票

TLDR

如果methodAawait上调用methodB,那么Promise返回的methodA将无法解决,直到Promise返回的methodB结算。

另一方面,如果methodA没有在await上调用methodB,那么Promise返回的methodA将立即解决Promise返回的methodB是否已经解决。

因此测试methodAawait上调用methodB只是测试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
  });
});


细节

在你的所有三个代码示例中,methodAmethodB都返回Promise

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

您正在测试的是promiseA等待解决,直到promiseB结算。


首先,让我们来看看如何测试promiseA不等promiseB


测试promiseA是否等待promiseB

一个简单的方法来测试负面情况(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();
  });

});

这是一个非常简洁,直接的测试。


如果我们可以返回相反的情况会很棒...如果此测试失败则返回true。

不幸的是,这不是一个合理的方法,因为如果promiseA await promiseB这个测试超时。

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


背景资料

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

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

ES6引入了PromiseJobs queue,它处理的工作“是对Promise的解决”。 PromiseJobs队列中的所有作业在当前消息完成之后和下一条消息开始之前运行。

因此,当Promise解析时,其then回调被添加到PromiseJobs队列,当当前消息完成时,PromiseJobs中的任何作业将按顺序运行,直到队列为空。

asyncawait只是syntactic sugar over promises and generators。在等待的await结算时,在Promise上调用Promise基本上将函数的其余部分包含在PromiseJobs中的调用中。


我们需要的是一个测试,告诉我们,如果promiseA DID等待promiseB,没有超时。

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

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

答案是使用PromiseJobs队列。

考虑这个测试:

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()返回一个已解析的Promise,因此两个回调会立即添加到PromiseJobs队列中。一旦当前消息(测试)暂停等待PromiseJobs中的作业,它们按照它们被添加到PromiseJobs队列的顺序运行,并且当测试继续运行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,以便我们稍后可以调用它。由于第一个Promise尚未解决,因此then回调不会立即添加到PromiseJobs队列中。另一方面,第二个Promise已经解决,所以它的then回调被添加到PromiseJobs队列。一旦发生这种情况,我们调用保存的resolve,以便第一个Promise解析,将其then回调添加到PromiseJobs队列的末尾。一旦当前消息(测试)暂停以等待PromiseJobs中的作业,order数组就会按预期包含['2', '1']


如果在函数调用中使用await,测试的智能方法是什么?

测试await是否用于函数调用的聪明方法是向thenpromiseA添加promiseB回调,然后延迟解析promiseB。如果promiseA等待promiseB,那么它的回调将永远是PromiseJobs队列中的最后一个。另一方面,如果promiseA不等待promiseB,那么它的回调将首先在PromiseJobs中排队。

最终解决方案在TLDR部分中。

请注意,当methodA是在async上调用awaitmethodB函数时,以及当methodA是一个正常(而不是async)函数返回Promise链接到Promise返回的methodB时,这种方法都有效(正如预期的那样,一次再次,async / await只是Promises和发电机的语法糖。


0
投票

我刚才有同样的想法:能否以编程方式检测异步函数会不会很好?事实证明,你做不到。如果你想获得可靠的结果,至少你不能这样做。

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

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,它称为reject

您可以很容易地看到这实际上和以前一样,因为您可以使用async定义一个函数,但是如果从外部调用if而不使用await,则使用promises的旧的then语法:

foo().then(...);

反过来也是如此:如果使用new Promise包装器定义函数,则可以使用await。所以,简而言之,asyncawait只是简洁的语法来做你需要手动完成的事情。

而这反过来意味着即使你用async定义一个函数,也绝对不能保证它实际上已经用await调用了!如果缺少await,这并不一定意味着它是一个错误 - 也许有人只是喜欢then语法。

因此,总而言之,即使您的问题有技术解决方案,它也无济于事,至少在所有情况下都不会如此,因为您不需要在不牺牲异步的情况下使用async调用await函数。

我知道在您的场景中,您希望确保实际等待承诺,但是恕我直言,然后您将花费大量时间来构建一个复杂的解决方案,但不能解决可能存在的所有问题。所以,从我个人的角度来看,这不值得付出努力。


-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返回的承诺。

使处理浮动承诺的目的复杂化的一件事是,有时开发人员有充分的理由让承诺浮动。所以任何检测方法都需要提供一种方法来说“这个浮动的承诺是可以的”。

您可以使用几种方法。

Use Type Analysis

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

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

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文件的类型信息,即使allowJscheckJs是真的。事情就这样发生了,有关于这个问题的a tslint issue,由某人提出(巧合!)碰巧想要在普通的JS文件上运行no-floating-promise规则。

因此,在我们发言时,为了能够从上面的检查中受益,您必须制作代码库TypeScript。

根据我的经验,一旦你运行了TypeScript和tslint设置,这将检测代码中的所有浮动承诺,并且不会报告虚假案例。即使你有一个承诺,你想在你的代码中留下浮动,你可以使用像tslint这样的// tslint:disable-next-line:no-floating-promises指令。并且,如果第三方库故意让承诺浮动,则无关紧要:您将tslint配置为仅报告代码问题,以便它不会报告第三方库中存在的问题。

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

Use a Promise Library That Detects Floating Promises at Runtime

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

问题是我不知道一个通​​常,可靠,同时满足这两个要求的promise库:

  1. 检测浮动承诺的所有情况。
  2. 不报告您不关心的案例。 (特别是,在第三方代码中浮动承诺。)

根据我的经验,配置promise库以改进它处理这两个要求之一的方式会损害它如何处理其他要求。

我最熟悉的诺言库是Bluebird。我能够检测到Bluebird的浮动承诺。然而,虽然你可以将Bluebird的承诺与遵循Promises / A +的框架产生的任何承诺混合,但是当你进行这种混合时,你会阻止Bluebird检测到一些浮动的承诺。您可以通过用Bluebird替换默认的Promise实现来提高检测所有案例的几率

  1. 明确使用第三方实现而不是本地实现的库(例如const Promise = require("some-spiffy-lib"))仍将使用该实现。因此,您可能无法在测试期间运行所有代码以使用Bluebird。
  2. 而且你可能最终会得到关于浮动承诺的虚假警告,这些承诺会故意留在第三方库中。 (请记住,有时开发人员会故意放弃承诺。)Bluebird不知道哪些是您的代码,哪些不是。它将报告它能够检测到的所有情况。在您自己的代码中,您可以向Bluebird表明您希望保留浮动,但在第三方代码中,您必须修改该代码以使警告静音。

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

© www.soinside.com 2019 - 2024. All rights reserved.