R50第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

异步交互测试

异步交互测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能为异步交互写出确定性测试:覆盖请求中、成功、失败、取消与重试五个状态,用 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 断言;重点观察「连发两次」时日志里旧响应被丢弃的过程。

动手任务

打开本课实验区:

  1. 为「失败 → 重试成功」写出完整三段式测试伪代码,标出 mock、findBy、断言各在哪一步。
  2. 用 fake timers 的思路改写:防抖 300ms 的输入,如何做到测试内 0 真实等待且只触发一次检索。
  3. 给「连发两次」补断言:第一次的响应落地时界面不变——写出你断言的具体可见内容。

常见错误

  • sleep 等待:概率性失败,CI 上最污染信心的来源;一切等待改条件式。
  • mock 忘了恢复:用例间互相污染;在 beforeEach/afterEach 里统一 restore。
  • 只测成功路径:失败、取消、重试才是线上事故高发区;五个状态都该有测试。
  • fake timers 忘了排空微任务:advanceTimers 后 promise 还没 resolve;用 advanceTimersByTimeAsync 或 runAllTimersAsync。

检查与复盘

自查清单:

  • 五个异步状态各有至少一条测试
  • 测试代码里搜不到裸 sleep,所有等待都是 findBy / waitFor
  • mock 的注入与恢复集中在统一的钩子里

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码简单

阅读测试代码(React Testing Library 风格)。点击「加载」按钮后数据要 800ms 才渲染出来。这个测试写法正确吗?

test('点击加载后显示文章标题', async () => {
  render(<ArticlePage />);
  fireEvent.click(screen.getByRole('button', { name: '加载' }));
  expect(await screen.findByText('React 性能优化指南')).toBeVisible();
});

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...