Vitest假定时器处理嵌套异步定时器(setTimeout)的问题咨询
我完全懂你碰到的这个坑——用Vitest假定时器测试这种循环嵌套的setTimeout时,普通的vi.runAllTimers()确实会漏掉后续动态创建的定时器,导致测试卡壳。
问题原因
vi.runAllTimers()的工作逻辑是一次性执行当前定时器队列里所有已存在的任务,但你的countDown函数里,第二个setTimeout是在第一个await sleep(1000)的回调执行完成后才被创建的。当你第一次调用vi.runAllTimers()时,第二个定时器还没被加入队列,所以根本不会被执行,这就导致countDown(2)返回的promise一直处于pending状态,测试就卡在await promise这一步了。
解决方案
针对这种动态创建定时器的场景,Vitest有专门的处理方式,下面给你两种可行的方案:
方案1:使用vi.runAllTimersAsync()(推荐)
Vitest提供了runAllTimersAsync()方法,它会先执行当前队列的定时器,然后等待所有异步操作完成(包括那些会创建新定时器的操作),直到没有更多待处理的定时器为止。修改你的测试代码如下:
it('works with runAllTimersAsync', async () => { const promise = countDown(2) await vi.runAllTimersAsync() // 改用runAllTimersAsync并await await promise // 现在不会卡住了 // 这里可以添加你的断言逻辑,比如验证倒计时的输出次数、最终状态等 })
方案2:手动循环处理定时器(兼容旧版本)
如果你使用的Vitest版本没有runAllTimersAsync,可以手动循环调用runAllTimers,同时等待事件循环处理异步回调,直到promise完成:
it('works with manual timer looping', async () => { const promise = countDown(2) // 循环执行直到promise状态变为fulfilled while (promise.status !== 'fulfilled') { vi.runAllTimers() // 给事件循环时间处理异步回调,生成新的定时器 await new Promise(resolve => setImmediate(resolve)) } await promise })
针对你实际场景的补充说明
你的真实需求是测试axios-retry的重试逻辑,本质和这个倒计时例子完全一致——重试的定时器是在前一次请求失败后才动态创建的。用runAllTimersAsync()就能完美解决这个问题,它会自动处理所有嵌套生成的重试定时器,让你的测试一次性跑完所有重试流程,不用等待真实时间。
内容来源于stack exchange

