跳转至

测试

编写测试以及在评审中要求测试的规则。AGENTS.md 在 Verifying 部分包含相同的编写规则;两者应同时修改。

测试什么

  • 测试可能出错的行为,例如编排(orchestration)与事件溯源(event-sourcing)、提供方协议转换、认证与权限守卫、持久化与迁移、外部格式解析、竞态修复,以及某次 bug 修复改动的逻辑。不要为类型已经保证的内容、琐碎逻辑、文案、冒烟运行、mock 调用镜像、回调接线,以及渲染为静态标记的 props 编写测试。
  • 先扩展,再新增。当现有测试或表覆盖该行为时,在其中添加一行或一个断言。只有没有任何用例适合时才编写新的 it。
  • 只测试一层:即 main 上会失败的最低层。不要在包装测试、每个调用点,或同一逻辑的 Web 和移动端副本中重复该用例。
  • 在评审中,只有当缺少的测试能捕获真正的回归时才要求补充。不要要求为每个常量、标签或 switch 分支都写一个断言。如果新测试违反了上述规则,请指出并说明修复方式:将其并入现有用例或表、删除它,或只保留一层。
  • 后端行为变更应附带有针对该行为的聚焦测试。
  • bug 修复的证明取决于 bug 本身。逻辑、数据、状态和协议类 bug 需要先有一个失败的测试——若模块测试文件的现有用例中有合适的,应使用数量最少、且因同一原因在 main 上失败的用例——然后进行修复。纯视觉 bug(间距、颜色、对齐、布局偏移)以其前后截图作为证明,且不需要测试:不要为琐碎细节添加 UI 测试,也不要在评审中要求添加截图已展示的 UI 测试。仅服务端、没有可见界面的修复需要附上测试以及简短的运行时记录。

等待工作完成

  • 服务器是事件溯源的,副作用在命令提交后运行。在测试中,排空 effect worker(OrchestrationEffectWorkerV2.drain),或等待标志着该里程碑的特定持久化事件或 Deferred。绝不要依赖 sleep 或轮询进行等待。需要超时才能通过的测试是错误的。具体来说:
  • it.effect 运行在 TestClock 上;Effect.sleep 只能通过 TestClock.adjust 推进。it.live 使用真实时钟,仅用于子进程或网络 fixture。
  • 不要使用 yieldNow 或 sleep 轮询循环。等待 Deferred,从 Queue 中取数据,或排空 worker。
  • 不要设置每测试超时参数或 retry。项目超时是上限;不稳定的测试应当被修复,而非重试。
  • 不要使用 performance.now() 或 Date.now() 的差值。对事件和计数进行断言,而不是对经过的时间进行断言。
  • 浏览器测试通过 expect.element 或 vi.waitFor 断言,绝不使用 requestAnimationFrame 或 transitionend。
  • vi.mock 工厂函数不得展开一个会引入 Shiki 或 diff workers 的真实模块。
  • it.skip 及其同类需要在其上方一行有一个跟踪 issue:// quarantine: #1234 <why>。
  • Lint 强制执行以下规则:t3code/no-test-timeout-or-unreasoned-skip, t3code/no-frame-waits-in-browser-tests, t3code/no-wall-clock-in-tests, t3code/no-real-time-wait-in-tests, t3code/no-polling-loop-in-tests。

Web 浏览器测试

  • Web 浏览器测试是 *.browser.test.tsx 或 *.browser.test.ts,并使用来自 apps/web/test/render.tsx 的 await using screen = await render(...) 进行渲染。t3code/require-disposable-render 会对旧模式报错。
  • 在渲染之后,获取测试在挂载后会驱动的资源(它推进的 fake timers、它在挂载后设置的 spies),以便这些资源在卸载前释放。首次渲染所读取的任何内容都必须放在渲染之前。
  • 一个先渲染然后等待或点击的辅助函数会将 screen 放入 AsyncDisposableStack,并且只在成功时将其 move() 出来,这样即使等待失败也仍会卸载。
  • rerender 每次调用都需要一个新的元素。
  • 不要使用生命周期钩子,也不要有模块级可变状态。vi.mock 工厂读取的共享 mock 状态由测试顶部的 setup() 调用重置。
  • router 默认开启。只有在带一行注释说明原因时才传 router: false。
it("saves on Enter", async () => {
  const onSave = vi.fn();
  using _now = vi.spyOn(Date, "now").mockReturnValue(0);
  await using screen = await render(<TitleEditor onSave={onSave} />, { router: "/threads/1" });
  await userEvent.type(screen.getByRole("textbox"), "Title{Enter}");
  expect(onSave).toHaveBeenCalledWith("Title");
});

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code