01测试产出总览

测试完成后交付三样东西:用例文档(发版回归的执行手册)、自动化用例代码(随代码同轮编写、可重复执行)、执行结果(见同页测试报告)。当前真实规模:

40 份
用例文档(用例_*.md)
464 个
浏览器 / 客户端 E2E 用例
39 套
API 契约测试(Playwright)
194 个
后端自动化断言(JUnit)
层载体跑的时机回答的问题
用例文档用例_*.md(编号 / 前置 / 步骤 / 断言 / 清理)每次发版按手册回归该验的都验了吗(覆盖面)
浏览器 / 客户端 E2EPlaywright + 浏览器驱动,真实窗口里点出来发版前用户真实路径通不通
API 契约测试Playwright 请求级断言每次提交接口行为符不符合契约
后端自动化断言JUnit(单元 / 控制器 / 集成)每次提交业务逻辑对不对

02用例文档长什么样

每份用例文档固定三段式:测试范围表 → 用例清单表(含状态列)→ 详细用例(前置 / 步骤 / 断言 / 清理)。以下为真实文档《用例_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-034SSE session.error 触发 ElMessage 通知错误提示展示异常流✅ Pass
TC-CHAT-036非流式 LLM 响应兜底listMessages 拉取完整内容异常流✅ Pass

③ 详细用例(关键用例展开为可执行步骤)

原文节选 · TC-CHAT-021 权限「永久允许」
前置:已绑定 workdir,有活跃 session;AI 将执行触发 permission.asked 的工具(如 bash rm)
步骤:
1. 发送触发权限请求的消息(如「请删除 /tmp/test.txt」)→ PermissionDialog 弹出
2. 点击「永久允许」
3. 再次发送触发同类权限的消息
断言:
- 首次弹窗后点击「永久允许」工具继续执行
- workdir 的 .yaoshu-code/approvals.json 新增按工具及目标范围约束的 allow 规则
- 第二次命中该范围规则的同类权限请求由 runtime 自动放行
- 范围外请求仍按智能审批或人工审批处理(不存在会话级全量自动批准)
清理:删除本用例写入的审批规则

03自动化用例代码

自动化用例不是孤立代码——spec 头部注释挂用例文档编号,代码与文档双向可追溯(哪条用例、验证什么维度、对应哪份设计文档):

Playwright 浏览器用例(真实代码原文节选 · chat-sse.spec.ts)
/**
 * 聊天 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('你好')
})
JUnit 后端用例(真实代码原文节选 · DiagnosticControllerTest.java)
/** 白名单默认密钥(明文,与 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) 静默跳过

04测试规约

测试代码自身的工程铁律(条文取自工程规范语料):

规约说明
命名分层执行单元测试 *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 关闭异步后同步断言,否则异步线程未完成写入导致竞态失败