javascript - 笑话:Timer 和 Promise 不能很好地工作。(setTimeout 和异步函数)
问题描述
关于此代码的任何想法
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.
但是,如果我await
从 LINE-A 中删除,则测试通过。
Promise 和 Timer 不能很好地工作吗?
我认为可能开玩笑的原因是等待第二个承诺解决。
解决方案
是的,你在正确的轨道上。
发生什么了
await simpleTimer(callback)
将等待返回的 PromisesimpleTimer()
解决,因此callback()
第一次被调用并且setTimeout()
也被调用。 jest.useFakeTimers()
替换setTimeout()
为模拟,因此模拟记录了它被调用的[ () => { simpleTimer(callback) }, 1000 ]
.
jest.advanceTimersByTime(8000)
运行() => { simpleTimer(callback) }
(从 1000 < 8000 开始)调用setTimer(callback)
whichcallback()
第二次调用并返回由await
. setTimeout()
不会第二次运行,因为其余的setTimer(callback)
都在队列中PromiseJobs
排队并且没有机会运行。
expect(callback).toHaveBeenCalledTimes(9)
callback()
只被调用两次的报告失败。
附加信息
这是一个很好的问题。它引起了人们对 JavaScript 的一些独特特性以及它在底层如何工作的关注。
消息队列
JavaScript 使用消息队列。在运行时返回队列以检索下一条消息之前,每条消息都会运行完成。setTimeout()
将消息添加到队列等功能。
作业队列
ES6 引入Job Queues
并且所需的作业队列之一是PromiseJobs
处理“作为对 Promise 解决的响应的作业”。此队列中的任何作业都在当前消息完成之后和下一条消息开始之前运行。
当调用它的 Promise 解决时,将then()
作业排队。PromiseJobs
异步/等待
async / await
只是 promises 和 generators 的语法糖。 async
总是返回一个 Promise 并且await
本质上将函数的其余部分包装在then
附加到给定 Promise 的回调中。
计时器模拟
Timer Mocks的工作原理是在调用时用 mocks替换函数setTimeout()
jest.useFakeTimers()
。这些模拟记录了它们被调用的参数。然后,当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
});
推荐阅读
- ldap - 如何通过具有属性值的成员在 ldap 中获取用户组
- php - 查询在 mysql 页面中运行,但不在 php 中
- php - 在 laravel 5.8 中发送验证邮件时自动设置 email_verified_at
- odoo - 如何从 Odoo 10 的上下文中更新/删除密钥?
- bash - 如何使用 bash 脚本将空行替换为特定文本?
- r - Cannot convert vector of different formats to POSIXct
- c++11 - 调用内核时 CUDA 中的类型不匹配
- django - 使用 Django 的唯一字段来减小数据库大小
- rust - 在异步任务中读取标准输入时,“必须从 Tokio 运行时的上下文中调用阻塞带注释的 I/O”
- r - 调整使用 lmerTest::lmer() 获得的 p 值以进行多重比较