在使用假计时器和承诺的组合时,我在让 Jest 测试框架(版本 23.2.0)正常工作时遇到了一些麻烦。我哪里错了?
假设我有以下模块:
// timing.js
export const timeout = ms =>
new Promise(resolve => {
setTimeout(resolve, ms)
})
我的测试文件如下所示:
// timing.test.js
import { timeout } from './timing'
describe('timeout()', () => {
beforeEach(() => {
jest.useFakeTimers()
})
it('resolves in a given amount of time', () => {
const spy = jest.fn()
timeout(100).then(spy)
expect(spy).not.toHaveBeenCalled()
jest.advanceTimersByTime(100)
expect(spy).toHaveBeenCalled()
})
})
此操作失败并显示以下输出:
● timeout › resolves in a given amount of time
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called, but it was not called.
15 |
16 | jest.advanceTimersByTime(100)
> 17 | expect(spy).toHaveBeenCalled()
| ^
18 | })
19 | })
20 |
at Object.<anonymous> (src/timing.test.js:17:17)
但是,如果我删除承诺:
// timing.js
export const timeout = ms => ({
then: resolve => {
setTimeout(resolve, ms)
}
})
...测试会通过
timeout
✓ resolves in a given amount of time (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.304s
更新
虽然这不是最优雅的解决方案,但我目前正在使用下面的测试。它有效,但我仍然很好奇为什么原来的不起作用
import { timeout } from './timing'
describe('timeout', () => {
it('resolves in a given amount of time', done => {
setTimeout(() => done(new Error('it didn\'t resolve or took longer than expected')), 10)
return timeout(9).then(done)
})
})
当前最好的替代方案是使用假定时器的异步版本。所以你会这样做
await clock.tickAsync(1000); // doesn't wait 1000ms but is async
而不是打电话给
clock.tick
。请参阅下面的答案了解更多详情。
您没有做错任何事 - 目前不起作用 - 抱歉。在此之前,必须先完成以下事情:
advanceTimeByTime(100)
之类的操作,并与 Promise 一起使用。要点的问题是
.then(spy)
只能在稍后被调用。
由于我们是志愿者 - 这些事情没有具体的时间表。我希望 SimenB 在未来 2-3 个月内完成合并,我将在下个月与 V8 团队跟进。
您始终可以编写异步测试:
// note this is an async function now
it('resolves in a given amount of time', async () => {
// this is in a promise.reoslve.then to not 'lock' on the await
Promise.resolve().then(() => jest.advanceTimersByTime(100));
await timeout(100);
});
如果还有其他想要等待的事情,您可以在超时后添加期望。
[email protected]
开始,您可以在两种不同的假定时器实现之间进行选择。
我发现
jest.useFakeTimers('legacy')
可以使用flushPromises
解决方法与Promises一起使用,但它不适用于Date
,而jest.useFakeTimers('modern')
可以与Date
一起使用,但不能与Promises一起使用,因为await flushPromises()
永远不会解析。
@sinonjs/fake-timers
来代替,因为它可以与 Promises 和 Date
一起使用,而无需任何解决方法或黑客:
import FakeTimers from "@sinonjs/fake-timers";
// Before tests:
const clock = FakeTimers.install();
// In tests:
await clock.tickAsync(100);
// After tests:
clock.uninstall();
在我的例子中,计时器回调调用了其他异步函数,因此其他解决方案对我不起作用。 我最终发现,通过手动确保承诺队列为空,所有异步代码都将完成运行,并且我可以让测试正常工作:
function flushPromises() {
// Wait for promises running in the non-async timer callback to complete.
// From https://stackoverflow.com/a/58716087/308237
return new Promise(resolve => setImmediate(resolve));
}
test('example', async () => {
jest.useFakeTimers();
example_function_to_set_a_timer();
// Wait for one virtual second
jest.advanceTimersByTime(1000);
// Wait for any async functions to finish running
await flushPromises();
// Continue with tests as normal
expect(...);
});
2023 年 3 月 6 日 Jest 在版本 29.5 中添加了异步计时器 API!
Jest 在内部依赖于Sinon 项目的 fake-timers
库,因此它实际上已经访问该功能已有五年了,但需要有人站出来并完成工作以将其公开在 Jest 实例上。直到现在才发生这种情况。 这意味着 Ben 的答案(从 2018 年开始)提到使用
@sinonjs/fake-timers
的
tickAsync(ms)
现在可以直接在 Jest 中使用:
await jest.advanceTimersByTimeAsync(6000)