测试¶
编写测试以及在评审中要求测试的规则。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