我想编写一个测试来检查我的函数是否使用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);
});
});
的聪明方法是什么?
答案 0 :(得分:0)
前段时间我有相同的想法:能够以编程方式检测异步函数会不会很好?原来,你不能。如果想获得可靠的结果,至少不能这样做。
其原因非常简单:async
和await
基本上是语法糖,由编译器提供。让我们看看在这两个新关键字存在之前,我们如何用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;
}
尽管我们现在可以使用throw
和return
,但实际情况与之前完全相同:编译器添加了一个return new Promise
包装器,并为每个return
添加了一个包装器。 ,它将调用resolve
,对于每个调用throw
的{{1}}。
您可以轻松地看到它实际上与以前相同,因为您可以使用reject
定义一个函数,但是如果从外部 without async
调用,通过使用承诺的良好旧await
语法:
then
反之亦然:如果使用foo().then(...);
包装器定义了函数,则可以new Promise
对其进行包装。因此,简而言之,await
和async
只是完成某些操作所需的巧妙语法。
这反过来意味着即使使用await
定义一个函数,也绝对没有{em> guarantee 实际上已经用async
调用了!而且,如果缺少await
,则不一定意味着它是一个错误-也许有人只是更喜欢await
语法。
总而言之,即使您的问题有一个技术解决方案,它也无济于事,至少在所有情况下都无济于事,因为您无需调用{{1 }}与then
一起使用,而不会牺牲异步性。
我了解,在您的情况下,您想确保已真正兑现了诺言,但是恕我直言,您随后花了很多时间来构建一个复杂的解决方案,但并未解决可能存在的所有问题。因此,从我个人的角度来看,这是不值得的。
答案 1 :(得分:0)
如果methodA
在await
上调用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 。
不幸的是,这不是一个合理的方法,因为如果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();
});
});
promiseA
,该测试就会超时。
我们将需要一种不同的方法。
背景信息
在继续之前,这里有一些有用的背景信息:
JavaScript使用message queue。下一条开始之前的当前消息runs to completion。 正在运行测试时,该测试是当前消息。
ES6引入了PromiseJobs queue来处理“响应承诺的解决”的工作。 PromiseJobs队列中的所有作业都在当前消息完成之后且下一条消息开始之前运行。
因此当await
解析时,其promiseB
回调将添加到PromiseJobs队列中,并且当当前消息完成时,PromiseJobs中的所有作业将在订购,直到队列为空。
Promise
和then
只是syntactic sugar over promises and generators。在async
上调用await
实际上将其余函数包装在待解决的await
解析后在PromiseJobs中安排的回调中。
我们需要的是一项测试,如果Promise
DID等待Promise
,它会告诉我们,而不会超时。
由于我们不希望测试超时,因此promiseA
和promiseB
都必须解决。
然后,目标是找出一种方法来判断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']
的明智方法是向await
和await
都添加一个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
以允许呼叫者链接承诺。在第二种情况下,您将使用await
将this.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库来改善它如何处理两个需求之一会损害它如何处理另一个需求。
我最熟悉的Promise库是Bluebird。我能够通过Bluebird检测到浮动承诺。但是,尽管可以将Bluebird承诺与Promises / A +之后的框架产生的任何承诺混合在一起,但是当您进行这种混合时,可以防止Bluebird检测到 some 浮动承诺。您可以通过将默认的tslint
实现替换为Bluebird但
显式使用第三方实现而不是本机实现(例如Promise
)的库仍将使用该实现。因此,在使用Bluebird的测试过程中,您可能无法使所有代码都运行。
您可能最终会收到关于第三方库中被故意浮动的浮动承诺的虚假警告。 (请记住,有时开发人员会故意放下承诺 。)Bluebird不知道哪个是您的代码,哪个不是。它将报告能够检测到的所有情况。在您自己的代码中,您可以向Bluebird表示您要使承诺保持浮动,但是在第三方代码中,您必须去修改该代码以使警告消失。
由于这些问题,我不会将这种方法用于严格检测浮动承诺。