有关此代码的任何想法
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback() // LINE-A without await here, test works as expected.
setTimeout(() => {
simpleTimer(callback)
}, 1000)
}
const callback = jest.fn()
await simpleTimer(callback)
jest.advanceTimersByTime(8000)
expect(callback).toHaveBeenCalledTimes(9)
}
```
失败
Expected mock function to have been called nine times, but it was called two times.
但是,如果我从LINE-A删除await
,则测试通过。
Promise和Timer工作不正常吗?
我认为开玩笑的原因可能是等待第二个承诺解决。
答案 0 :(得分:24)
是的,您在正确的轨道上。
await simpleTimer(callback)
将等待simpleTimer()
返回的Promise解析,因此callback()
会被首次调用,而setTimeout()
也将被调用。 jest.useFakeTimers()
replaced setTimeout()
with a mock,因此该模拟记录记录了它是通过[ () => { simpleTimer(callback) }, 1000 ]
调用的。
jest.advanceTimersByTime(8000)
运行() => { simpleTimer(callback) }
(自1000 <8000开始),后者调用setTimer(callback)
,后者再次调用callback()
,并返回由await
创建的Promise。 setTimeout()
自setTimer(callback)
is queued in the PromiseJobs
queue的其余部分以来没有第二次运行,并且没有机会运行。
expect(callback).toHaveBeenCalledTimes(9)
无法报告callback()
仅被两次调用。
这是一个好问题。它引起人们对JavaScript的一些独特特征及其在幕后的工作方式的关注。
消息队列
JavaScript使用a message queue。在运行时返回队列以检索下一条消息之前,每条消息都是run to completion。像setTimeout()
add messages to the queue之类的功能。
工作队列
ES6 introduces Job Queues
,并且所需的作业队列之一是PromiseJobs
,该队列处理“作为对Promise结算的响应的作业”。在当前消息完成之后且在下一条消息开始之前,该队列中的所有作业都运行 。
当在解决方案上调用Promise时,then()
在PromiseJobs
中将作业排队。
异步/等待
async / await
is just syntactic sugar over promises and generators。 async
总是返回Promise,而await
本质上将其余函数包装在附加到给定Promise的then
回调中。
计时器模拟
Timer Mocks由replacing functions like setTimeout()
with mocks工作,当调用jest.useFakeTimers()
时。这些模拟记录了调用它们的参数。然后,在调用jest.advanceTimersByTime()
时,将运行一个循环,该循环同步调用在经过的时间内已计划的所有回调,包括在运行这些回调时添加的任何回调。
换句话说,setTimeout()
通常将必须等待直到当前消息完成才能运行的消息排队。计时器提示允许回调在当前消息中同步运行。
下面是一个演示上述信息的示例:
jest.useFakeTimers();
test('execution order', async () => {
const order = [];
order.push('1');
setTimeout(() => { order.push('6'); }, 0);
const promise = new Promise(resolve => {
order.push('2');
resolve();
}).then(() => {
order.push('4');
});
order.push('3');
await promise;
order.push('5');
jest.advanceTimersByTime(0);
expect(order).toEqual([ '1', '2', '3', '4', '5', '6' ]);
});
Timer Mocks将同步执行回调,但是这些回调可能导致作业在PromiseJobs
中排队。
幸运的是,让PromiseJobs
中所有待处理的作业在async
测试中运行实际上很容易,您所需要做的就是调用await Promise.resolve()
。这实际上会将其余测试排在PromiseJobs
队列的末尾,并使队列中已有的所有内容先运行。
请记住,这是该测试的有效版本:
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback();
setTimeout(() => {
simpleTimer(callback);
}, 1000);
}
const callback = jest.fn();
await simpleTimer(callback);
for(let i = 0; i < 8; i++) {
jest.advanceTimersByTime(1000);
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
expect(callback).toHaveBeenCalledTimes(9); // SUCCESS
});
答案 1 :(得分:1)
布莱恩·亚当斯的答案就在眼前。
但是呼叫await Promise.resolve()
似乎只能解决一个未决的诺言。
在现实世界中,如果必须在每次迭代中一次又一次地调用此表达式,则测试具有多个异步调用的函数将很痛苦。
相反,如果您的函数有多个await
,则使用jwbay's response会更容易:
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
await flushPromises()
的地方,都致电await Promise.resolve()
答案 2 :(得分:0)
有一个用例我只是找不到解决方案:
function action(){
return new Promise(function(resolve, reject){
let poll
(function run(){
callAPI().then(function(resp){
if (resp.completed) {
resolve(response)
return
}
poll = setTimeout(run, 100)
})
})()
})
}
测试如下:
jest.useFakeTimers()
const promise = action()
// jest.advanceTimersByTime(1000) // this won't work because the timer is not created
await expect(promise).resolves.toEqual(({completed:true})
// jest.advanceTimersByTime(1000) // this won't work either because the promise will never resolve
基本上,除非计时器提前,否则操作不会解决。这里感觉像是循环依赖:promise需要计时器来解决,伪计时器需要promise来解决。
答案 3 :(得分:0)
以上真的很有帮助!对于那些尝试使用 React hooks(!) 执行此操作的人,以下代码对我们有用:
// hook
export const useApi = () => {
const apis = useCallback(
async () => {
await Promise.all([
new Promise((resolve) => {
api().then(resolve);
}),
new Promise((resolve) => {
return setTimeout(() => {
resolve();
}, 10000);
}),
]);
},
[],
);
return [apis];
}
// test
import { renderHook, act } from '@testing-library/react-hooks';
function flushPromises() {
return new Promise((resolve) => setImmediate(resolve))
}
it('tests useApi', async () => {
jest.useFakeTimers();
const { result } = renderHook(() => useApi());
api.mockReturnValue(Promise.resolve());
await act(async () => {
const promise = result.current[0]()
await flushPromises()
jest.runAllTimers()
return promise
})
});