如何正确使用Jest测试结合async/await的JavaScript定时器逻辑
Jest 测试带定时器的异步轮询函数的正确实现
场景说明
待测试的业务逻辑是一段依赖内置定时器实现轮询的异步代码,测试目标是验证轮询循环的执行逻辑是否符合预期,但现有写法始终因为异步时序错位导致断言失败。
被测函数代码如下:
async function makeStartedProcess() { const extProc = await startExternalProcess(); // 循环检查进程是否处于启动状态 while (await isProcessStarting(extProc)) { // 间隔1000ms后发起下一次状态检查 await new Promise(r => setTimeout(r, 1000)); } return extProc }
最初的测试思路是通过动态修改isProcessStarting的mock返回值,模拟轮询过程中的状态变化,测试代码大致如下:
beforeAll(() => { jest.useFakeTimers() }) test('loop', (done) => { startExternalProcess.mockResolvedValue({ foo:1 }) isProcessStarting.mockResolvedValue(false) makeStartedProcess().then(p => { // 校验最终返回的进程实例 done() }) // 期望第一次状态检查已经完成 expect(isProcessStarting).toBeCalledTimes(1) // 切换后续状态检查的返回值为"正在启动" isProcessStarting.mockResolvedValue(true) // 推进定时器时间1200ms jest.advanceTimersByTime(1200) })
时序错位的核心原因:async函数中的每一个await都会将后续代码拆分为待执行的微任务,测试代码和被测函数的执行顺序并不是从上到下同步对齐的。单纯靠假定时器推进时间,无法自动驱动这些微任务按顺序执行,导致断言触发时,被测函数还没有跑到预期的执行节点。
正确的测试实现方案
核心调整思路:
- 摒弃传统
done回调写法,改用async/await语法编写测试,显式控制每一个异步阶段的执行时机 - 利用Jest提供的异步版假定时器API,自动等待定时器回调触发过程中产生的微任务执行完成
- 预设mock的返回值序列,替代中途手动修改mock返回值的写法,避免修改时机错位
- 在每个关键执行节点做断言,确保时序符合预期
调整后的测试代码示例:
beforeEach(() => { // 每个测试前重置定时器和mock状态,避免测试间的状态污染 jest.useFakeTimers() jest.clearAllMocks() }) test('轮询逻辑会在进程启动完成后返回实例', async () => { const mockExtProc = { foo: 1 } startExternalProcess.mockResolvedValue(mockExtProc) // 按调用顺序预设状态检查的返回值:前两次返回"正在启动",第三次返回"启动完成" isProcessStarting .mockResolvedValueOnce(true) .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) // 先拿到被测函数的promise,暂不await等待最终结果 const processPromise = makeStartedProcess() // 等待第一轮微任务执行:外部进程启动完成,完成第一次状态检查 await Promise.resolve() expect(isProcessStarting).toHaveBeenCalledTimes(1) expect(isProcessStarting).toHaveBeenCalledWith(mockExtProc) // 推进1000ms定时器,等待第一次轮询的等待周期结束 await jest.advanceTimersByTimeAsync(1000) // 等待微任务执行完成:发起第二次状态检查 expect(isProcessStarting).toHaveBeenCalledTimes(2) // 再推进1000ms定时器,等待第二次轮询的等待周期结束 await jest.advanceTimersByTimeAsync(1000) // 等待微任务执行完成:发起第三次状态检查 expect(isProcessStarting).toHaveBeenCalledTimes(3) // 等待被测函数退出循环、返回最终结果 const result = await processPromise expect(result).toBe(mockExtProc) })
实践注意事项
- 优先使用
jest.advanceTimersByTimeAsync替代同步版本的advanceTimersByTime:前者会自动清空推进定时器过程中产生的所有微任务队列,不需要手动多次写await Promise.resolve(),时序控制更可靠 - 不要在测试流的中间位置手动修改mock的返回值:用
mockResolvedValueOnce按调用顺序预设返回值,逻辑更清晰,也不会因为异步时序错位,导致mock修改的时机晚于被测代码的执行时机 - 每一个异步执行阶段都要显式await等待完成,再做对应断言,避免出现“断言执行时被测代码还没跑到对应节点”的错位问题
内容的提问来源于stack exchange,提问作者djm181
相关产品推荐
相关产品推荐

