尧图精选

异步逻辑单测:fake timers 与 Promise 处理

🕒 发布时间:2026/9/13 11:25:01 📁 来源:尧图网络
异步逻辑单测fake timers 与 Promise 处理在前端工程的单元测试Unit Testing中超过80% 的生产级缺陷都潜伏在“复杂的异步时序交互”之中防抖节流debounce/throttle在用户连续输入时是否能正确合并请求接口轮询Polling在收到特定成功状态码后是否能立即终止定时器竞态请求Race Conditions在新请求返回时是否能主动抛弃旧请求的脏数据异步超时Timeout在网络挂起超过 5 秒时是否能正确触发降级重试当大模型为这些异步逻辑生成 Vitest / Jest 单测时最常见的两个致命翻车现场是写了真实的await sleep(3000)导致单测在 CI 里慢如蜗牛跑 10 个测试文件要等 2 分钟搞混了宏任务与微任务MacroTask vs MicroTask的刷新时序在fakeTimers快进时间后由于 Promise 微任务队列未被及时清空断言在数据更新前提前执行导致单测频繁发生“时序假失败Flaky Tests”。本文将系统拆解在 Vitest 中编写工业级异步单测的核心范式与底层时序控制。异步单测的时序全景宏任务、微任务与假定时器[JavaScript 异步任务双队列拓扑] ├── 宏任务队列 (MacroTask Queue) ── setTimeout, setInterval, MessageChannel, I/O └── 微任务队列 (MicroTask Queue) ── Promise.then(), queueMicrotask(), async/await ──────────────────────────────────────────────────────────────────────── [Vitest 假定时器 (Fake Timers) 时钟快进机制] ├── 1. vi.useFakeTimers(): 劫持全局 Date.now, setTimeout, setInterval ├── 2. vi.advanceTimersByTime(1000): 虚拟时钟瞬间快进 1 秒 (真实耗时 0 毫秒!) └── 3. await Promise.resolve(): 核心必须强制清空微任务队列等待 DOM/State 刷新核心模式一防抖Debounce与时间快进标准单测被测试的业务防抖搜索函数// src/utils/useDebouncedSearch.ts import { ref } from vue; export function useDebouncedSearch(searchApi: (q: string) Promisestring[], delay 300) { const results refstring[]([]); const isSearching ref(false); let timer: any null; const search (query: string) { clearTimeout(timer); isSearching.value true; timer setTimeout(async () { try { const data await searchApi(query); results.value data; } finally { isSearching.value false; } }, delay); }; return { results, isSearching, search }; }AI 生成的工业级严密测试代码// src/utils/__tests__/useDebouncedSearch.spec.ts import { describe, it, expect, vi, beforeEach, afterEach } from vitest; import { useDebouncedSearch } from ../useDebouncedSearch; describe(useDebouncedSearch 异步防抖单测, () { beforeEach(() { // 1. 开启虚拟时钟劫持 vi.useFakeTimers(); }); afterEach(() { // 2. 测试完毕后必须恢复真实时钟防止污染其他测试文件 vi.useRealTimers(); }); it(连续高频触发时仅应在防抖时间窗口结束后发起最后一次请求, async () { const mockSearchApi vi.fn().mockResolvedValue([苹果, 香蕉]); const { results, isSearching, search } useDebouncedSearch(mockSearchApi, 300); // 连续触发 3 次搜索 search(a); search(ap); search(app); // 此时尚未到达 300ms 延迟API 严禁被调用 expect(mockSearchApi).not.toHaveBeenCalled(); expect(isSearching.value).toBe(true); // 模拟时间快进 200ms (仍未达标) vi.advanceTimersByTime(200); expect(mockSearchApi).not.toHaveBeenCalled(); // 模拟再快进 100ms (达到 300ms 阈值) vi.advanceTimersByTime(100); // 关键定时器触发后内部是 async 函数必须等待微任务队列清空 await vi.runAllTicksAsync(); // 断言仅发起了 1 次最终入参为 app 的请求 expect(mockSearchApi).toHaveBeenCalledTimes(1); expect(mockSearchApi).toHaveBeenCalledWith(app); expect(results.value).toEqual([苹果, 香蕉]); expect(isSearching.value).toBe(false); }); });核心模式二Promise 竞态与取消Race Condition Guard在快速切换 Tab 标签页时先发出的慢请求绝对不能覆盖后发出的快请求it(当存在并发竞态时先发出的慢请求返回时不应覆盖最新请求的数据, async () { // 构造两个可手动控制 resolve 时机的手动 Promise let resolveFirst: (data: string) void; const firstPromise new Promisestring((r) { resolveFirst r; }); let resolveSecond: (data: string) void; const secondPromise new Promisestring((r) { resolveSecond r; }); const mockApi vi.fn() .mockReturnValueOnce(firstPromise) .mockReturnValueOnce(secondPromise); const { data, loadTab } useTabSwitcher(mockApi); // 1. 触发第一次请求 (Tab 1) loadTab(tab1); // 2. 紧接着立即切换到 Tab 2 loadTab(tab2); // 3. 模拟异常时序Tab 1 的慢请求在 2 秒后才返回 resolveFirst!(Tab 1 旧数据); await flushPromises(); // 等待微任务清空 // 断言由于 Tab 2 已经触发Tab 1 的旧数据必须被静默丢弃 expect(data.value).not.toBe(Tab 1 旧数据); // 4. Tab 2 请求正常返回 resolveSecond!(Tab 2 最新数据); await flushPromises(); expect(data.value).toBe(Tab 2 最新数据); });异步测试常用工具函数对照表在编写异步测试时牢记这 4 个 Vitest 专属 API 的分工API 方法作用机制适用典型场景vi.useFakeTimers()拦截全局所有原生定时器必须在beforeEach中前置声明vi.advanceTimersByTime(ms)虚拟时钟快进指定毫秒数测试setTimeout、setInterval、防抖节流flushPromises()强制排空当前微任务队列等待fetch、Promise.then回调执行完毕vi.runAllTicksAsync()快进宏任务并同步排空微任务解决定时器内部嵌套 async/await 时的时序等待异步测试 Prompt 注入规范为了防止大模型生成包含硬编码sleep的低劣单测在 Prompt 中施加以下铁律# 异步单测编写底线Async Testing Strict Rules 1. 严禁真实等待严禁使用 new Promise(r setTimeout(r, 1000)) 进行真实时间阻塞必须统一使用 vi.useFakeTimers() 与 vi.advanceTimersByTime() 2. 状态断言前必须 flush在快进时间或 mock 请求返回后断言前必须加上 await flushPromises() 确保微任务完成 3. 必须成对清理在 afterEach 中必须显式调用 vi.useRealTimers() 与 vi.clearAllMocks()。落地收益单测执行极速化全仓 180 个包含复杂防抖、轮询与长超时的异步单测全量执行耗时从原本的45 秒骤降至 1.4 秒全部在虚拟内存时钟中瞬间跑通杜绝时序偶发挂起彻底消灭了因 CI 容器 CPU 负载波动导致的 Promise 竞态假报错。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →