测试完成后交付三样东西:用例文档(发版回归的执行手册)、自动化用例代码(随代码同轮编写、可重复执行)、执行结果(见同页测试报告)。当前真实规模:
| 层 | 载体 | 跑的时机 | 回答的问题 |
|---|---|---|---|
| 用例文档 | 用例_*.md(编号 / 前置 / 步骤 / 断言 / 清理) | 每次发版按手册回归 | 该验的都验了吗(覆盖面) |
| 浏览器 / 客户端 E2E | Playwright + 浏览器驱动,真实窗口里点出来 | 发版前 | 用户真实路径通不通 |
| API 契约测试 | Playwright 请求级断言 | 每次提交 | 接口行为符不符合契约 |
| 后端自动化断言 | JUnit(单元 / 控制器 / 集成) | 每次提交 | 业务逻辑对不对 |
每份用例文档固定三段式:测试范围表 → 用例清单表(含状态列)→ 详细用例(前置 / 步骤 / 断言 / 清理)。以下为真实文档《用例_AI对话与SSE》的原文节选:
| 维度 | 覆盖点 |
|---|---|
| 消息发送 | 文本输入、Ctrl/Cmd+Enter 发送、附件上传、multimodal 标识 |
| SSE 流式 | 文本增量、reasoning、工具调用、step.ended、session.idle |
| 权限弹窗 | permission.asked 事件、PermissionDialog 三选项(once/always/reject) |
| 流式滚动 | 粘底 / 上滚打断、新消息悬浮按钮 |
| 错误处理 | SSE error、session.error |
| 编号 | 场景 | 验证维度 | 分类 | 状态 |
|---|---|---|---|---|
TC-CHAT-001 | 发送纯文本消息,收到流式回复 | text.delta/ended、助手消息渲染 | 正向流 | ✅ Pass |
TC-CHAT-011 | 流式中点击中断按钮 | POST interrupt、状态恢复 | 正向流 | ✅ Pass |
TC-CHAT-021 | 权限弹窗「永久允许」(always) | 后续同类权限不弹窗 | 正向流 | ✅ Pass |
TC-CHAT-027 | 重试 assistant 消息 | revert + 重发原 user 文本 | 组合流 | ✅ Pass |
TC-CHAT-034 | SSE session.error 触发 ElMessage 通知 | 错误提示展示 | 异常流 | ✅ Pass |
TC-CHAT-036 | 非流式 LLM 响应兜底 | listMessages 拉取完整内容 | 异常流 | ✅ Pass |
前置:已绑定 workdir,有活跃 session;AI 将执行触发 permission.asked 的工具(如 bash rm) 步骤: 1. 发送触发权限请求的消息(如「请删除 /tmp/test.txt」)→ PermissionDialog 弹出 2. 点击「永久允许」 3. 再次发送触发同类权限的消息 断言: - 首次弹窗后点击「永久允许」工具继续执行 - workdir 的 .yaoshu-code/approvals.json 新增按工具及目标范围约束的 allow 规则 - 第二次命中该范围规则的同类权限请求由 runtime 自动放行 - 范围外请求仍按智能审批或人工审批处理(不存在会话级全量自动批准) 清理:删除本用例写入的审批规则
自动化用例不是孤立代码——spec 头部注释挂用例文档编号,代码与文档双向可追溯(哪条用例、验证什么维度、对应哪份设计文档):
/** * 聊天 SSE 浏览器 E2E 测试。 * 选择器来源:前端组件源码(MessageComposer / MessageList / PermissionDialog…)。 * * 绝对禁止: * - if (await xxx.isVisible().catch(() => false)) 静默跳过 * - expect(await xxx.count() >= 0).toBe(true) 废话断言 * * 需要真实 session 的用例:sidecar 未连接无法创建时用 test.fail() 明确标记。 */ test('TC-CHAT-002 Ctrl/Cmd+Enter 快捷发送', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }) await page.goto(workdirUrl) await page.locator(INPUT_SELECTOR).fill('你好') await page.keyboard.press('Control+Enter') // 用户消息立即上屏;助手消息流式累积;session.idle 后发送按钮恢复 await expect(page.locator(MESSAGE_LIST_SELECTOR)).toContainText('你好') })
/** 白名单默认密钥(明文,与 application-dev.yml 的 diagnostic.viewer-keys 对齐) */ private static final String DEVELOPER_KEY = "爻枢未来"; @Test @DisplayName("白名单为空:正确密钥也拒绝(fail-closed,配置缺失不静默放行)") void requireViewer_emptyWhitelist_rejected() throws Exception { properties.setViewerKeys(List.of()); // 未配置密钥 = 全部拒绝 invokeRequireViewerAndExpectReject( requestWithAuth("Bearer " + encodedDeveloperKey()), "密钥无效"); }
// 家规一:用例名直读场景——失败时看名字就知道坏了什么,不用读代码 @DisplayName("白名单为空:正确密钥也拒绝(fail-closed,配置缺失不静默放行)") // 家规二:断言给到具体值,禁止废话断言(count() >= 0 永真 = 没测) assertTrue(msg.contains("密钥无效"), "提示语应含「密钥无效」,实际: " + msg); // 家规三:环境不满足用 test.fail() 明确标记,禁止 isVisible().catch(() => false) 静默跳过
测试代码自身的工程铁律(条文取自工程规范语料):
| 规约 | 说明 |
|---|---|
| 命名分层执行 | 单元测试 *Test.java 由 Surefire(mvn test)执行;集成测试 *IT.java 由 Failsafe(mvn verify)执行。禁止仅执行 mvn test 后宣称全量通过——*IT.java 不被 Surefire 拾取,会静默跳过产出「测试通过」假象 |
| MockMvc 事务不回滚 | MockMvc 请求在独立线程执行,Controller 事务在请求完成后即提交——Service 直调测试用 @Transactional 自动回滚;MockMvc 测试在 @AfterEach 中 TRUNCATE TABLE 手动清理 |
| 异常场景断言形态 | 全局异常处理器捕获所有异常返回 HTTP 200 + flag=false(而非 5xx)——异常断言必须 status().isOk() + jsonPath("$.flag").value(false),禁止期望 is5xxServerError() |
Mock 用 @MockBean | 集成测试 Mock 外部依赖用 @MockBean(Spring Boot 官方机制),禁止 @TestConfiguration @Bean 手动创建同类型 Bean——会与容器已有 Bean 冲突 |
| 异步模块同步化 | 异步模块的测试必须经 @TestPropertySource 关闭异步后同步断言,否则异步线程未完成写入导致竞态失败 |