T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

测试 ​

编写测试的规则,以及在评审里怎样要求补测试。AGENTS.md 在 Verifying(验证)一节有相同的编写规则;两处一起改。

测什么 ​

  • 测试可能坏掉的行为,例如编排和事件溯源、提供方(provider)协议转换、认证和权限守卫、持久化与迁移、外部格式解析、竞态修复,以及某次缺陷修复改动的逻辑。不要为这些写测试:类型已经保证的事、琐碎逻辑、文案、冒烟运行、对 mock 调用的镜像、回调接线,以及渲染成静态标记的 props。
  • 先扩展,再新增。已有的测试或表格覆盖了这个行为,就在那里加一行或一条断言。只有没有任何用例合适时,才新写一个 it。
  • 只在一层测:会在 main 上失败的最低那一层。不要在包装测试里、每个调用点,或同一套逻辑的 Web 与移动端副本里把这个用例再写一遍。
  • 评审时,只有缺的这条测试能抓住真正的回归,才要求补上。不要要求每个常量、每条标签或每个 switch 分支都有一条断言。新测试若违反上面的规则,指出来,并写明改法:并进已有的用例或表格、删掉,或只留一层。
  • 后端行为有改动时,带上针对这个行为的聚焦测试一起交付。
  • 缺陷修复拿什么当证明,取决于缺陷本身。逻辑、数据、状态和协议上的缺陷,先写失败的测试(条数取最少,而且它们是因为这个原因在 main 上失败;该模块测试文件里已有合适的用例时,就加在那里),然后再修复。纯视觉缺陷(间距、颜色、对齐、布局偏移)用改前和改后的截图当证明,不写测试:不要为细小的视觉问题加 UI 测试;截图已经显示出来时,评审里也不要再要一条。没有可见界面的纯服务器修复,交付测试,再附一段简短的运行时记录。

等待工作完成 ​

  • 服务器采用事件溯源,副作用在命令(command)提交之后才运行。测试里要排空效应工作器(OrchestrationEffectWorkerV2.drain),或者等待标志着这个里程碑的那条已持久化事件(event),或等待 Deferred。不要靠 sleep 或轮询来等。得靠超时才能通过的测试就是错的。具体如下:
    • it.effect 在 TestClock 上运行;Effect.sleep 只能通过 TestClock.adjust 向前推进。it.live 走墙上时钟,只用于子进程或网络夹具。
    • 不要写 yieldNow 或 sleep 的轮询循环。去等待 Deferred,从 Queue 取出,或者排空工作器。
    • 不要给单个测试加超时参数,也不要 retry。项目的超时是上限;不稳定的测试要修,不要重试。
    • 不要用 performance.now() 或 Date.now() 的差值。断言事件和计数,不要断言经过的时间。
    • 浏览器测试通过 expect.element 或 vi.waitFor 做断言,绝不用 requestAnimationFrame 或 transitionend。
    • vi.mock 工厂不得展开一个会拉进 Shiki 或 diff 工作器的真实模块。
    • 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 报错。

    • 测试在挂载之后才去驱动的东西(要往前拨的模拟定时器、挂载之后才设置的 spy),在渲染之后再取得,这样它们会在卸载之前释放。第一次渲染就会读到的东西,放在渲染之前。
    • 先渲染再等待或点击的辅助函数,把界面放进 AsyncDisposableStack,并且只在成功时才用 move() 把它移出来,所以等待失败时仍然会卸载。
    • rerender 每次调用都要一个新元素。
    • 不要用生命周期 hook,也不要模块级的可变状态。vi.mock 工厂会读到的共享 mock 状态,在测试开头用一次 setup() 调用重置。
    • router 默认开启。只有旁边用一行注释说明原因时,才传入 router: false。
    tsx
    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");
    });

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools