测试策略
Gemini CLI — 测试策略
学习目标
- 理解 Gemini CLI 的多层测试架构(单元、集成、性能)
- 掌握 TestRig 集成测试夹具的使用模式
- 分析 MSW 网络拦截和 SVG 快照测试的应用
项目实践
测试层次
测试金字塔: ┌──────────┐ │ 性能测试 │ ← perf-tests/ (少量) ┌┴──────────┴┐ │ 集成测试 │ ← integration-tests/ + evals/ (中等) ┌┴────────────┴┐ │ 单元测试 │ ← *.test.ts (大量) └──────────────┘| 层次 | 文件位置 | 数量级 | 执行时间 |
|---|---|---|---|
| 单元测试 | packages/*/src/**/*.test.ts | 数百 | 秒级 |
| 集成测试 | integration-tests/ | 数十 | 分钟级 |
| 性能测试 | perf-tests/ | 少量 | 分钟级 |
| 评估测试 | evals/ | 数十 | 分钟级 |
测试基础设施
Vitest:所有包使用 vitest.config.ts 配置测试环境:
export default defineConfig({ test: { setupFiles: ['./test-setup.ts'], environment: 'node', globals: true, },});test-setup.ts:每个包的测试环境初始化:
- Mock 环境变量
- 设置全局配置
- 初始化 MSW 服务器
MSW(Mock Service Worker)网络拦截
packages/core/src/mocks/msw.ts 使用 MSW 拦截网络请求:
import { setupServer } from 'msw/node';import { handlers } from './handlers';
const server = setupServer(...handlers);
// 测试前启动beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));// 测试后清理afterEach(() => server.resetHandlers());afterAll(() => server.close());优势:
- 拦截真实的
fetch调用,而非 Mockfetch函数 - 更接近真实网络行为(包括 URL 匹配、请求体检查)
- 支持流式响应 Mock(对 LLM API 很重要)
Handler 示例(Gemini API Mock):
http.post('https://generativelanguage.googleapis.com/**', () => { return HttpResponse.json({ candidates: [{ content: { parts: [{ text: 'Hello!' }] } }] });});TestRig 集成测试夹具
TestRig(packages/test-utils/src/test-rig.ts)提供完整的 AgentLoopContext:
const rig = await TestRig.create({ config: { model: 'gemini-2.5-flash', approvalMode: 'yolo', }, tools: [readFileTool, shellTool, grepTool],});
await rig.initialize();
// 使用 rig.context 进行集成测试const client = new GeminiClient(rig.context);const stream = client.sendMessageStream('Hello', signal, 'test-prompt');
for await (const event of stream) { // 验证事件流}TestRig 提供的功能:
- 完整的
AgentLoopContext(Config、ToolRegistry、MessageBus、Scheduler) - Mock 文件系统
- Mock Gemini API 响应
- 自动清理(测试后删除临时文件)
Memory Test Harness
MemoryTestHarness(packages/test-utils/src/memory-test-harness.ts)用于检测内存泄漏:
const harness = new MemoryTestHarness();harness.start();// 执行操作harness.measure();// 报告内存增长测量方法:
- 操作前记录 V8 堆快照
- 操作后记录 V8 堆快照
- 比较两次快照,识别未释放的对象
- 检测特定类型的泄漏(如事件监听器未清理)
Perf Test Harness
PerfTestHarness(packages/test-utils/src/perf-test-harness.ts)用于性能基准测试:
const harness = new PerfTestHarness();harness.startTimer('tool-execution');await executeTool(toolCall);harness.stopTimer('tool-execution');harness.report();测量指标:
- 工具执行延迟
- 上下文渲染时间
- 模型路由延迟
- 总 turn 时间
SVG Snapshot Testing
TUI 组件使用 SVG 快照进行可视化回归测试:
test('should render SVG snapshot for a shell tool', async () => { const { container } = render(<ToolGroup type="shell" command="ls -la" />); expect(container).toMatchSVGSnapshot();});快照文件:存储在 __snapshots__/*.snap.svg 文件中,可通过浏览器直接查看。
为什么不是文本快照:
- 终端 UI 使用 Unicode 字符和 ANSI 颜色代码
- 文本 diff 无法捕获渲染差异(如边框错位、颜色变化)
- SVG 快照可以在浏览器中预览,方便审查
Integration Tests
integration-tests/ 目录包含端到端测试:
integration-tests/├── test-fixtures/ # 测试夹具(项目目录结构)├── basic-chat.test.ts # 基本对话测试├── tool-execution.test.ts # 工具执行测试└── ...测试模式:
- 创建临时项目目录(使用 test-fixtures 中的模板)
- 启动 Gemini CLI 进程(子进程)
- 通过 stdin/stdout 发送输入和读取输出
- 验证 Agent 行为(工具调用、文件修改等)
Evals
evals/ 目录包含评估测试,验证 Agent 在特定场景下的行为质量:
| 评估文件 | 测试内容 |
|---|---|
memory_persistence.eval.ts | 记忆持久化的正确性 |
auto_memory_contract.eval.ts | 自动记忆提取的契约验证 |
auto_memory_modes.eval.ts | 不同记忆模式的行为 |
陷阱与对策
MSW 与流式响应的兼容性
问题:MSW 默认不支持流式响应 Mock,但 LLM API 返回的是 SSE 流。
对策:
- 使用自定义 handler 模拟 SSE 流
- 将完整响应分块发送,模拟真实流式行为
- 或在测试中关闭流式模式,使用非流式 Mock
TestRig 初始化成本高
问题:TestRig 需要初始化完整的 AgentLoopContext,单个测试文件的 setup 代码量大。
对策:
- 使用
beforeAll共享 TestRig 实例 - 每个测试用例重置状态而非重新初始化
- 对于轻量测试,使用 Mock 对象而非 TestRig
SVG 快照的 CI 不稳定性
问题:SVG 快照对终端尺寸和 Unicode 字体敏感,CI 环境与本地可能不同。
对策:
- CI 中设置固定的终端尺寸(
process.env.COLUMNS = '120') - 快照使用相对渲染,不依赖绝对坐标
- 定期在 CI 上更新快照,避免 drift
设计取舍
MSW vs 手动 Mock
| 方案 | 优势 | 代价 |
|---|---|---|
| MSW | 拦截真实 fetch、URL 匹配、更接近真实行为 | 需要额外依赖、流式响应需要定制 |
| 手动 Mock | 简单直接、完全控制 | 容易遗漏边界情况、与实际 API 行为可能不一致 |
Gemini CLI 的选择:MSW。LLM API 的行为复杂(流式、错误格式、重试),手动 Mock 容易与实际行为不一致。