用途
本文档是一份测试规范(Testing Guidelines),用于指导项目中的测试编写方式,统一测试理念、命令与测试文件布局约定。它面向需要新增功能、修复缺陷或维护测试的开发者,帮助其判断该写哪类测试、放在哪里、如何运行与验证。
测试理念
文档主张优先采用两类测试:
- 真正的集成测试——使用真实运行时、真实文件系统、真实网络调用,不使用 mock、stub 或 fake,用以证明系统端到端可用。
- 针对纯逻辑或隔离逻辑的单元测试——测试输入输出清晰的纯函数或高度隔离的模块,因代码无外部依赖,无需 mock。
单元测试以 .test.ts[x] 文件与所测源码同目录存放。集成测试位于 tests/ 目录,主要测试装置包括:
tests/ipc——依赖 IPC、聚焦后端行为的测试。tests/ui——使用真实 IPC 与 happy-dom 全应用渲染的前端集成测试。tests/e2e——使用 Playwright 的端到端测试,用于验证 happy-dom 难以覆盖的浏览器行为。
此外,src/browser/stories 中的 stories 主要用于人工视觉验证 UI 变更。
Mock 使用原则
避免大量使用 mock 的测试,因为这类测试验证的是实现细节而非行为。如果需要 mock 才能测试,应考虑重构代码使其更易测试。文档指出一个例外:存在 mockAiRouter 可用于模拟 LLM 响应。测试中 LLM 的使用遵循以下规则:
- 验证与 LLM 提供商集成的测试使用真实 LLM(例如断言正确识别上下文超限错误)。
- 验证 LLM 逻辑周边行为的测试使用 mockAiRouter(例如断言 LLM 响应后消息正确排队)。
- 完全不涉及智能体行为的逻辑,不使用真实 LLM 或 mockAiRouter(例如测试打开终端窗口是否工作)。
同时避免同义反复的测试(简单映射、实现代码的完全复制),聚焦不变量与边界失败。
何时测试
理想情况下所有新功能和缺陷都应被良好测试。没有稳健的测试策略就不要实现功能或修复。修复缺陷时始终从测试开始(TDD 实践):先在测试中复现缺陷,再修复生产代码,最后验证测试通过。
测试运行方式
tests/ 中的所有测试在设置 TEST_INTEGRATION=1 的情况下通过 bun x jest 运行。位于 src/ 中的测试(通常为单元测试)通过 bun test 运行。
运行时与检查
- 切勿终止正在运行的 Xum 进程;本地验证依赖以下命令:
make typecheckmake static-check(快速本地 lint/typecheck/fmt 路径;仅 oxlint,无类型感知 lint 规则)make static-check-full(增加对所有文件的类型感知 ESLint、文档链接与 CI 中使用的 bench-agent 验证)- 定向测试调用(例如
bun x jest tests/ipc/sendMessage.test.ts -t "pattern")
- 仅在本地定向检查通过后才等待 CI 通过。
- 优先使用外科手术式的定向测试调用,而非运行完整测试套件。
- 保持工具函数纯净,或将外部副作用参数化,以便测试。
覆盖率期望
- 优先采用简化现有代码的修复;此类简化通常不需要新测试。
- 增加复杂度时,应新增或扩展测试。若覆盖率需要新基础设施,先提出测试装置方案,再在其中添加测试。
- 被要求 TDD 时,编写真实的仓库测试(不使用
/tmp脚本)并提交。 - 将复杂逻辑抽取到易于测试的工具函数中。以最少用例实现广泛覆盖,证明功能确实重要。
Storybook
- 确保所有 UI 变更至少被一个 story 覆盖(许多变更已被现有 story 覆盖)。
- 仅使用全应用 story(
App.*.stories.tsx)。不要添加孤立组件 story,即使是很小的 UI 变更(本仓库不使用也不接受)。 - 使用
storybook/test工具(within、userEvent、waitFor)的 play 函数与 UI 交互并设置所需视觉状态。不要仅为 storybook 便利而给生产组件添加 props。 - 保持 story 数据确定性:避免在 story 设置中使用
Math.random()、Date.now()等非确定性值。当顺序或时序对视觉稳定性重要时,传入显式值。 - 滚动稳定:在改变元素尺寸的异步操作(Shiki 高亮、Mermaid 渲染、工具展开)之后,等待
useAutoScroll的 ResizeObserver RAF 完成。使用双 RAF:await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))。
UI 测试(tests/ui)
tests/ui中的测试必须通过AppLoader渲染完整应用,并从用户视角驱动交互(点击、输入、导航)。- 使用
renderReviewPanel()辅助函数或类似模式渲染<AppLoader client={apiClient} />。 - 不要在此测试孤立组件或工具函数——它们应作为单元测试放在实现旁边(
*.test.ts)。 - 切勿直接调用后端 API(例如
env.orpc.workspace.remove())来触发正在测试的操作——始终模拟用户操作(点击删除按钮等)。直接调用 API 会绕过导航、状态更新和错误处理等前端逻辑,而这些往往是缺陷藏身之处。- 后端 API 调用可用于设置/清理或避免昂贵操作。
- 若需细粒度测试后端逻辑,考虑将测试移至
tests/ipc。
- 这些测试中切勿绕过 UI;例如不要调用
updatePersistedState更改 UI 状态——应通过 UI 触发所需行为。 - 这些测试需要
TEST_INTEGRATION=1;使用shouldRunIntegrationTests()守卫。 - 仅在确实发起 AI API 调用的测试中调用
validateApiKeys()。纯 UI 交互测试(点击按钮、选择项目)不需要 API 密钥。
happy-dom 限制
- Radix Portal 内容渲染在
document.body中——happy-dom 不会将其放在view.container下,因此限定在应用根节点的查询会漏掉对话框/弹出框内容。 - 变通方法: 对于基于 portal 的 UI(Dialog/Popover/Tooltip),查询
view.container.ownerDocument.body(或within(document.body))并在那里驱动交互。输入优先使用userEvent以确保受控输入更新。 - 若 portal 内容因缺少浏览器 API 仍不出现,优先使用条件渲染(
{isOpen && <div>...}),如AgentModePicker那样,或回退到 tests/e2e(启动时间约 2 分钟)。
测试辅助函数约定
- 使用非 portal 组件时,在
view.container内查询元素(而非document.body)。 - 使用带明确错误信息的
waitFor()以辅助调试:if (!el) throw new Error("Element not found")。 - 辅助函数按用户操作命名:
openBaseSelectorDropdown()、selectSuggestion(),而非实现细节。
IPC 测试(tests/ipc)
尽量完全通过 IPC 交互测试后端。避免在此直接断言或修改后端状态。例外包括:
- 构建大量历史以测试原本昂贵的操作(例如长上下文处理)。
- 测试可观察副作用属于 API 契约一部分的逻辑,例如 createProject 在项目目录不存在时创建该目录。
确定性
尽量减少测试的竞态。测试运行于多种环境,包括负载沉重的 CI 运行器。优先使用显式同步而非任意 sleep。当显式同步不可行时,使用 waitFor 等模式,它在常见情况下能快速完成。
例外
在某些情况下,由于基础设施或性能限制,可能选择偏离这些指南。此时应确保测试代码(或缺少测试的生产代码)有良好注释,说明例外背后的理由。