异步交互测试
异步交互测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完你能为异步交互写出确定性测试:覆盖请求中、成功、失败、取消与重试五个状态,用 mock 控制响应时机、用 fake timers 消除真实等待、用 findBy/waitFor 等待条件而不是睡眠。
问题场景
课程列表的异步筛选测试,第一版靠 await new Promise((r) => setTimeout(r, 500)) 等请求:本地网慢一点就过,CI 快一点就红;改一下延迟参数,测试全线崩。异步测试的不稳定,几乎都源于同一个错误:用「等待时间」代替「等待条件」。真实网络时序不可控,可控的测试必须把时序从等式里消掉。
核心解释
确定性来自三个控制点。
**一、mock 网络层,不碰真实网络。**被测组件调用 fetchCourses,测试里把它换成可编程的假实现——何时 resolve、何时 reject、延迟多久,全部由测试说了算:
test("失败后点重试能拿到数据", async () => {
let calls = 0;
vi.mocked(fetchCourses).mockImplementation(() =>
++calls === 1 ? Promise.reject(new Error("超时")) : Promise.resolve(courses)
);
render(<CourseList />);
expect(await screen.findByText(/加载失败/)).toBeInTheDocument(); // 第一次:失败
await userEvent.click(screen.getByRole("button", { name: "重试" }));
expect(await screen.findByText("React 入门")).toBeInTheDocument(); // 重试成功
expect(calls).toBe(2);
});
**二、等待条件,不等待时间。**findByText = 渲染 + 轮询直到元素出现(或超时失败),把「多久到达」交给框架;waitFor 断言条件;裸 sleep 是最后才考虑的手段。
**三、fake timers 接管时钟。**防抖、轮询这类逻辑用假时钟虚走,测试毫秒级完成且绝对稳定:
vi.useFakeTimers();
render(<SearchList />);
await userEvent.type(screen.getByRole("textbox"), "abc"); // 防抖 300ms
expect(fetchSpy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(300); // 虚走 300ms(异步排空微任务)
expect(fetchSpy).toHaveBeenCalledTimes(1);
五个状态的覆盖清单:请求中(加载提示可见)、成功(数据可见)、失败(错误提示加重试入口)、取消(发起两次操作,第一次的结果被丢弃——断言只有第二次的 UI)、重试(失败后能走到成功)。取消态最容易漏:请求竞态的守卫没有测试盯着,重构时最容易悄悄坏掉。
示例拆解
实验区是一个异步列表加「测试场景台」:
- 场景选择器:请求中(60s 挂起)/ 成功 / 失败 / 慢然后成功;
- 「发起请求」与「连发两次」按钮:后者用于验证竞态守卫——旧响应落地时已被更新的请求取代,事件日志里能看到「丢弃」;
- 每个场景下方列出对应的测试断言写法(用 getByText / findByText 的视角);
- 验证:把五个场景各跑一遍,为每个场景写出一行 findBy 断言;重点观察「连发两次」时日志里旧响应被丢弃的过程。
动手任务
打开本课实验区:
- 为「失败 → 重试成功」写出完整三段式测试伪代码,标出 mock、findBy、断言各在哪一步。
- 用 fake timers 的思路改写:防抖 300ms 的输入,如何做到测试内 0 真实等待且只触发一次检索。
- 给「连发两次」补断言:第一次的响应落地时界面不变——写出你断言的具体可见内容。
常见错误
- sleep 等待:概率性失败,CI 上最污染信心的来源;一切等待改条件式。
- mock 忘了恢复:用例间互相污染;在 beforeEach/afterEach 里统一 restore。
- 只测成功路径:失败、取消、重试才是线上事故高发区;五个状态都该有测试。
- fake timers 忘了排空微任务:advanceTimers 后 promise 还没 resolve;用 advanceTimersByTimeAsync 或 runAllTimersAsync。
检查与复盘
自查清单:
- 五个异步状态各有至少一条测试
- 测试代码里搜不到裸 sleep,所有等待都是 findBy / waitFor
- mock 的注入与恢复集中在统一的钩子里
参考资料
- Testing Library:异步工具 —— findBy 与 waitFor 的等待语义
- Vitest:vi 工具 —— mock 与 fake timers
- Testing Library:user-event —— 真实交互模拟
读完这一课了?
标记完成后,工作台会安排接下来的学习。