You need to enable JavaScript to run this app. 如何正确使用Jest测试结合async/await的JavaScript定时器逻辑
优惠活动
大模型
产品
解决方案
定价
更多

如何正确使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 10:27:03