测试策略

**核心判断:测试不是「凭 agent.md」或「凭 skill」单一决定的,而是四层共同决定 —— `agent.md` 定标准、skill 约束方法、package scripts 执行、tests + CI 判定;问题的根源正是这四层没对齐(规则说 API 测试需要本地服务,`pnpm test` 和 CI 却把它当普通单元测试直接跑)。**

一、测试由四层共同决定

  1. agent.md —— 定「什么情况下验证什么」 项目级测试政策:发布前必须 lint/test/build;UI 改动要验深色/浅色;API 改动要验认证与用户数据权限。回答「标准是什么、为什么要测」。

  2. skill —— 约束「如何严谨地测」 TDD(行为改动先建失败测试再实现)、systematic-debugging(失败先找根因,不直接改断言碰运气)、verification-before-completion(没有新鲜测试证据不算完成)。

  3. 项目脚本 —— 「实际执行哪些测试」 pnpm lint / pnpm test / pnpm build,机器实际入口。文档里写「运行测试」,最终必须落到这些可执行命令。

  4. 测试文件与 CI —— 「判定条件与远端环境」 tests/**/*.test.* 是断言;GitHub Actions 决定远端启动什么服务、注入什么变量。Vercel 生产构建 ≠ GitHub CI ≠ API 集成测试。

关系链:agent.md 定标准 → skill 约束方法 → scripts 执行 → tests 判定 → CI 在远端复现。

坑:标准写了但执行入口没对齐,规则就等于没生效。 agent.md 已说明 API 集成测试依赖本地服务,脚本/CI 却把它当普通单元测试直接执行。

二、断言要检查行为,不是实现

失败的三条断言都在检查「代码长什么样」,而不是「用户行为是否正确」:

  • 要求脚本名「完全相等」→ 新增合法脚本也会失败
  • 匹配旧 CSS 固定值(8px),实现已改为共享变量 --frame
  • 要求旧的 createdAt 排序文本,实现已改为按发布时间排序

断言失败不能直接改测试,先判断归因:

  • 需求没变、实现错了 → 改实现
  • 需求变了 → 才更新断言

三、竞态用例要用状态确认制造并发,不能用固定 sleep

「剪藏并发刷新锁」用例失败得到 [200, 200],预期 [200, 409]。用例只是发第一个请求后固定等 50ms 再发第二个,没有确认第一个请求已经把数据库状态改成 fetching。开发服务器冷编译、鉴权或数据库较慢时,第二个请求进入锁判断前第一次刷新可能已经结束 —— 此时返回两个 200 是合理的。

正确做法:先轮询数据库确认状态已变为 fetching,再发第二个请求。 若这样仍得到两个 200,才能证明锁实现有问题。

判据:「等待固定时长」是用时间代理「前置条件已成立」,是典型的代理量(见 判据要用真值而非代理)。

另外注意失败计数口径:这不是两个独立 Bug,而是同一个子测试失败 + 外层测试组随之失败被统计了两次。报「2 个用例失败」时先看是不是同一根因被重复计数(见 口径先于数值)。

四、死锁用例:要按真实时序编排,而不是让测试自己去抢锁

「logout 用例挂住」的现象:测试启动 logout 后 await /logout 请求,但 logout 内部先等 FIFO 队列里的 credentialStore.clear(),而该 clear 排在被测试阻塞住的 refresh 的 replace 之后 —— 测试不 release,replace 不结束;replace 不结束,clear 不执行;clear 不执行,logout 的 HTTP 请求永不发出。互等。

修法(只改测试编排,不动生产代码):按队列真实推进顺序显式同步 ——

  1. 启动 logout;
  2. await controller.waitUntilCredentialOperationsQueued(atLeast: 4) —— 先确认本地清理已进入队列;
  3. 再 await store.releaseReplace() —— 释放被阻塞的写;
  4. 最后 await logout.value 与 refresh.value。

禁止在 releaseReplace 之前等待 logout 的 HTTP request —— 那正是死锁点。

判据:「等待某个信号」必须发生在该信号真的可能发生之后。 与本节第三条同源:用固定 sleep 或错误顺序等待,都是拿时序代理前置条件;死锁是这种错误的最极端形态(不只是等不到,而是互相等)。

五、三层测试各自回答什么

  • API 集成测试:「后端接口和数据库逻辑会不会回归?」—— 把真实后端、数据库、缓存启动起来,像客户端一样发 HTTP 请求,验证认证、权限、数据写入、响应字段和错误分支。
  • Preview(部署后验收):「部署后的完整应用能不能给人验收?」
  • 单元测试:「某个函数或模块自己的逻辑对不对?」

因此临时数据库不是产品 Preview 数据库的替代品,而是测试运行时使用的隔离试验场。集成测试的自动化编排通常是:起临时服务 → 建表 → 起真实服务 → 注册唯一测试账号 → 打各 API → 校验 → 删账号并关服务。

坑:测试写好了但没进 CI —— 等于只在手动跑的时候才存在。

六、集成阶段:把模块串起来跑,不是重造功能

集成不是再开发一套新功能,而是把已经各自通过测试的模块真正串起来跑一遍 —— 证明「零件都合格」之后「整台机器也能工作」。

完整链路(以 Agent 面板为例):浏览器面板 → Web BFF(鉴权与转发)→ Agent Server → Runtime/模型 → Tool/Skill/写操作确认 → 数据库持久化 → SSE 流式返回 → 前端 Transcript → 观测平台。集成要在这条真实链路上验证:

  • 把各模块代码合入正式 main,从 main 启动同一套服务(Web / Agent / 数据库 / 观测),而不是各模块自己那套环境;
  • 用真实账号跑完整场景:普通对话、Tool 成功与失败、写操作确认与取消、刷新后恢复、多会话切换、重试与幂等、Usage/Trace/错误记录是否正确;
  • 做 fault injection(故障注入):模型不可用、Agent 进程重启、SSE 断线、重复 clientRequestId、观测平台不可用 —— 确认故障不会造成重复写入、串会话、空消息或敏感信息泄漏;
  • 记录最终 commit、镜像、migration、环境配置与回滚步骤。

两个判据:

  1. 模块级测试通过 ≠ 整台机器能工作 —— 集成专门暴露「只有连接起来才会出现」的问题(单模块测试里两个模块都绿,连起来仍可能不一致)。
  2. 集成 ≠ 已知还有一堆功能 Bug —— 它是最后的总装、压力测试和发布验收,不是补做功能。

与「部署级验证」的分工:本节讲验证什么(真实链路 + 故障注入 + 回滚记录),开发流程要按任务大小分级 讲什么时候做(代码级验证过了只够提 PR;部署形态变了才必须做部署级验证)。

相关

  • 开发流程要按任务大小分级 —— 验证强度该按任务大小分级,而不是所有事走同一种重流程
  • 判据要用真值而非代理 —— 「检查代码长什么样」是用代理量(实现形状)代替真值(用户行为)
  • 分层要落到运行时才有效 —— 标准只写在文档里、没落到执行入口,约束等于不存在
  • 通过只证明它实际驱动的路径 —— 本页「六、集成阶段」是该 synthesis 的第二处证据:模块级通过只驱动了各自那条路