笑话:Timer 和 Promise 不能很好地工作。 (setTimeout 和异步函数)
Posted
技术标签:
【中文标题】笑话:Timer 和 Promise 不能很好地工作。 (setTimeout 和异步函数)【英文标题】:Jest: Timer and Promise don't work well. (setTimeout and async function) 【发布时间】:2019-02-10 03:45:26 【问题描述】:关于此代码的任何想法
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 都不好用吗?
我认为开玩笑的原因可能是等待第二个承诺解决。
【问题讨论】:
【参考方案1】:是的,你在正确的轨道上。
会发生什么
await simpleTimer(callback)
将等待simpleTimer()
返回的 Promise 解决,因此callback()
被第一次调用,setTimeout()
也被调用。 jest.useFakeTimers()
replaced setTimeout()
with a mock 所以模拟记录它是用[ () => simpleTimer(callback) , 1000 ]
调用的。
jest.advanceTimersByTime(8000)
运行 () => simpleTimer(callback)
(因为 1000 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 解析时,then()
在 PromiseJobs
中排队作业。
异步/等待
async / await
is just syntactic sugar over promises and generators。 async
总是返回一个 Promise,await
基本上将函数的其余部分包装在一个附加到给定 Promise 的 then
回调中。
定时器模拟
Timer Mocks 在调用jest.useFakeTimers()
时由replacing functions like setTimeout()
with mocks 工作。这些模拟记录了它们被调用的参数。然后,当调用 jest.advanceTimersByTime()
时,会运行一个循环,该循环会同步调用任何在经过的时间内安排的回调,包括在运行回调时添加的任何回调。
换句话说,setTimeout()
通常将必须等到当前消息完成才能运行的消息排队。 Timer Mocks 允许回调在当前消息中同步运行。
这是一个演示上述信息的示例:
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 和 Promise 发挥出色
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
);
【讨论】:
很好的答案!谢谢布赖恩。还有一些文章将 JavaScriptmessage queue
和 job queue
称为 macro tasks
和 micro tasks
,以避免混淆。
这样一个伟大而彻底的答案。拯救了我的理智!
具体来说,这对我有用await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
,谢谢!
这似乎是关于将承诺与 Jest 假计时器混合的主题的最佳答案,但我根本不明白,这东西对我没有任何意义。有人有更多资源可以帮助我理解吗? Jest docs 分别处理承诺和时间,但我需要测试混合情况..【参考方案2】:
有一个用例我只是找不到解决方案:
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 需要 timer 来推进来解决,fake timer 需要 promise 来解决来推进。
【讨论】:
找到了这个解决方法,似乎是唯一稳定的工作方法:github.com/facebook/jest/issues/7151#issuecomment-463370069 我遇到了完全相同的问题,代码非常相似。我通过执行await Promise.resolve(); jest.runAllTimers(); await Promise.resolve();
使其工作,这使正在测试的代码将第一个setTimeout
放入队列中。然后,您可以像往常一样提前计时器来执行测试。
@RobinElvin 你能用这个技巧告诉我你的完整工作代码吗?我已经被几乎相同的轮询式代码困了几个小时。非常感谢:)
@GlennMohammad 我似乎再也找不到该代码了。但本质上,您在测试中使用await Promise.resolve();
来让步给其他代码,以便它可以处理队列。它有助于了解超时和承诺的确切顺序,以便您可以编排您的测试。在我的示例中,我让待处理的承诺解决,然后能够运行待处理的计时器,这反过来又产生了另一个可以通过再次等待来“刷新”的承诺。
@RobinElvin 非常感谢您的努力。根据您的见解,我设法通过使用相同的构造使其在昨晚(实际上是凌晨 2:30 ?)工作。由于我无法真正确定何时必须“启动”第一个 await Promise.resolve()
以在我的 poll 函数中进行让步,因此我必须在 while
循环中执行此操作,就像 @nemo 指向的链接中的循环一样。另外,同时使用nock
对我来说也是个问题。这是可能对未来读者有用的 sn-p:gist.github.com/dwiyatci/740a52a08eb6147baa1f0be9b4f38785 ?【参考方案3】:
Brian Adams 的answer 是正确的。
但调用 await Promise.resolve()
似乎只能解决一个未决的承诺。
在现实世界中,如果我们必须在每次迭代中一遍又一遍地调用这个表达式,那么测试具有多个异步调用的函数会很痛苦。
相反,如果您的函数有多个 await
s,这样做会更容易:
在某处创建此函数
开玩笑
function flushPromises()
return new Promise(resolve => setImmediate(resolve));
笑话 >= v27
function flushPromises()
return new Promise(jest.requireActual("timers").setImmediate)
现在调用await flushPromises()
,否则你会调用多个await Promise.resolve()
s
有关this GitHub 问题的更多详细信息。
【讨论】:
开玩笑>= v27:function flushPromises() return new Promise(jest.requireActual("timers").setImmediate)
credit
其实对于 Jest >= v27 正确的函数是:function flushPromises() new Promise(resolve => jest.requireActual('timers').setImmediate(resolve));
Jest 26.6,这不适用于 jest.useFakeTimers('modern')
,但适用于 jest.useFakeTimers('legacy')
。请参阅this answer。【参考方案4】:
以上内容真的很有帮助!对于那些试图用 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
)
);
【讨论】:
【参考方案5】:我偶然发现了同样的问题并最终直接使用@sinonjs/fake-timers
,因为它提供了clock.tickAsync()
功能,根据文档:
tickAsync() 也会中断事件循环,允许任何预定的 Promise 回调在运行计时器之前执行。
工作示例现在变为:
const FakeTimers = require('@sinonjs/fake-timers');
const clock = FakeTimers.install()
it('simpleTimer', async () =>
async function simpleTimer(callback)
await callback()
setTimeout(() =>
simpleTimer(callback)
, 1000)
const callback = jest.fn()
await simpleTimer(callback)
await clock.tickAsync(8000)
expect(callback).toHaveBeenCalledTimes(9) // SUCCESS \o/
);
【讨论】:
以上是关于笑话:Timer 和 Promise 不能很好地工作。 (setTimeout 和异步函数)的主要内容,如果未能解决你的问题,请参考以下文章
笑话 - mockReturnValue : Promise<boolean>