CHAPTER 10 · 全栈
工程化与质量保障
当代码大部分由 Agent 编写,你的核心工作之一就变成了建立一张自动化的安全网:让错误在几秒钟内、以明确的方式暴露出来,而不是靠你逐行阅读。这张网同时也是 Agent 的“眼睛”——它靠测试结果知道自己写对了没有。
- 说出验证金字塔每一层能抓住什么错误、代价是多少。
- 写出一个 Vitest 单元测试和一个 Playwright 端到端测试。
- 配置一个在每个 PR 上运行类型检查、Lint、测试和构建的 CI 流水线。
- 识别 Agent 在测试上的典型“作弊”行为。
开场:你不可能读完所有代码
Agent 一天之内为 LinkNote 提交了 30 次修改,涉及 80 个文件。你认真审查了其中最重要的 5 个。凭什么相信另外 75 个文件没有把已有功能改坏?
答案方向:你不需要相信 Agent,你需要相信检查。把“什么叫做正确”编码成机器可以执行的检查(类型、规则、测试),每次修改都自动运行。然后你的审查重点就从“这 80 个文件对不对”转移到“这些检查是否覆盖了重要的行为”和“这 5 个关键设计决策对不对”。
验证金字塔
对于 Agent 工作流,最底下两层的价值被放大了:它们在几秒钟内给出结果,Agent 可以在一个循环里反复“修改 → 检查 → 修复”,而不需要你介入。
静态检查
类型检查
第 01、04 章已经强调过:tsc --noEmit 必须零错误,并且在 CI 中强制执行。TypeScript 7 之后,大项目的类型检查也只需要几秒钟。
Lint 与格式化
两种主流组合:
- Biome:一个 Rust 写的工具同时负责格式化和 Lint,极快,零配置可用。新项目的好默认值。
- ESLint(typescript-eslint)+ Prettier:生态最丰富,尤其是需要类型信息的规则(如
no-floating-promises检测遗漏的 await、no-misused-promises)。
值得开启的规则都有一个共同点:它们针对的是“能运行但有隐患”的代码——正是 Agent 容易写出、人又容易看漏的那一类。另外,Knip 可以找出未使用的文件、导出和依赖,Agent 反复修改后特别容易留下这类垃圾。知道
单元测试:Vitest 会用
Vitest 是 Vite 生态的测试框架,API 与 Jest 基本相同,原生支持 TS 和 ESM,速度很快。
好测试的特征:测的是行为(输入 → 输出),而不是实现细节;名字就是需求描述;覆盖边界(空值、大小写、空列表)。pnpm vitest 进入监听模式,改代码后自动重跑。
对 React 组件,可以用 Testing Library 按“用户能看到的东西”查询和操作(按角色、按文字),而不是按 CSS 类名。对 Cloudflare Workers,有专门的 Vitest 集成可以在 workerd 运行时里跑测试并模拟绑定。
端到端测试:Playwright 会用
Playwright 启动真实的浏览器,像用户一样点击、输入、断言。它覆盖了单元测试覆盖不到的东西:页面之间的跳转、前后端的集成、样式导致的元素不可点击等。
注意测试是按角色和标签定位元素的(getByRole("button", { name: "保存" }))。这不仅让测试更稳定,也顺便检查了第 05 章说的语义化 HTML——如果“按钮”是个 div,这个测试就找不到它。
Playwright 失败时会保存trace(每一步的截图、DOM、网络请求),用 pnpm playwright show-trace 打开,是排查 E2E 失败的利器。很多 Coding Agent 也可以通过 Playwright(或浏览器 MCP 工具)直接“看到”页面,做视觉层面的自我检查。
Agent 时代的测试策略
- 测试就是规格。对于重要功能,先让 Agent 写测试(或者你写测试的标题列表),确认测试表达了你要的行为,再让它实现。这就是测试驱动开发(TDD),在 Agent 工作流里它的价值比以往更高:测试把模糊的需求变成了可执行的验收标准。
- 警惕“改测试让它通过”。Agent 在修不好代码时,有时会修改测试的断言、加上
.skip、或者把期望值改成当前的错误输出。审查 diff 时,对测试文件的修改要格外仔细看。可以在 AGENTS.md 里写明:“除非我要求,不得修改已有测试的断言”。 - 小心过度 mock。把数据库、网络全都 mock 掉的测试,测的往往只是 mock 本身。涉及 SQL、鉴权的逻辑,用真实数据库(本地 Docker Postgres、测试用 D1、或 PGlite 这类内存 Postgres)写集成测试更有价值。
- 快照测试少用。大块的快照很容易被“一键更新”,失去检验意义。
CI:把检查变成门禁 会用
然后在 GitHub 仓库设置里开启分支保护:主分支只能通过 PR 合并,且 CI 必须通过。这样,不论是你还是 Agent,都无法把没通过检查的代码合进主分支。E2E 测试可以在部署平台生成预览 URL 后,针对预览环境运行。
Git 习惯
- 小步提交:让 Agent 每完成一个可验证的步骤就提交一次。出问题时,你可以精确地回退到上一个好的状态。
- 一个功能一个分支 / PR:PR 描述写清楚做了什么、为什么、怎么验证。可以要求 Agent 按模板生成 PR 描述。
- 并行开发:同时让多个 Agent 处理不同任务时,用
git worktree为每个任务创建独立的工作目录,避免互相干扰。知道
Monorepo 知道
当项目包含多个应用和共享代码(例如 Web 前端、独立 API、共享的类型与 schema 包),可以用 pnpm workspaces 把它们放在一个仓库里,用 Turborepo 按依赖关系编排构建、测试并缓存结果。
不要过早引入。只有一个应用时,普通的单仓库结构就足够了。第一次需要“两个部署单元共享代码”时,再迁移。
可观测性:上线后你怎么知道它坏了 知道
| 手段 | 作用 | 工具举例 |
|---|---|---|
| 结构化日志 | 用 JSON 记录事件和上下文(用户 ID、请求 ID),便于检索 | 平台日志(Vercel Logs、wrangler tail、Workers Logs) |
| 错误追踪 | 自动收集前后端异常、堆栈、影响的用户数 | Sentry 等 |
| 链路追踪 | 看一次请求在各服务、数据库、LLM 调用上的耗时 | OpenTelemetry 标准 + 各类后端 |
| 前端性能 | 真实用户的加载速度和交互延迟(LCP、INP、CLS) | Lighthouse、平台分析工具 |
| LLM 可观测性 | 记录每次模型调用的输入、输出、token、延迟、成本 | 第 11 章 |
把下面这样的“完成定义(Definition of Done)”放进 AGENTS.md,每个任务都适用:
- 单元测试、集成测试、E2E 测试怎么分配?按金字塔:大量快速的单元测试覆盖逻辑分支;少量集成测试覆盖与数据库、外部服务的契约;极少量 E2E 覆盖核心用户流程(注册、下单等)。
- 如何测试依赖时间或随机数的代码?把时间和随机源作为参数注入,或使用测试框架的假时钟(
vi.useFakeTimers())。 - CI 里应该跑什么?安装(锁文件冻结)→ 类型检查 → Lint → 测试 → 构建;主分支保护;E2E 针对预览环境。
自检
1. Agent 修复一个 bug 后报告“所有测试通过”。diff 中测试文件有一处修改:expect(result).toBe(3) 改成了 expect(result).toBe(2)。你应该怎么处理?
先判断哪个值才符合需求。如果 3 是正确的业务结果,那么 Agent 是在“修改测试去迎合错误的实现”,应当回退这处修改并要求修复实现。如果需求确实变了,这个修改是合理的,但应该在总结中被明确说明。无论哪种情况,都不能只看“测试通过”这个结论。
2. 一个团队只有 E2E 测试,没有单元测试。每次 CI 需要 25 分钟,而且经常因为网络波动失败。这会对 Agent 工作流造成什么影响?
Agent 获得反馈的周期变得很长(25 分钟一次),无法快速迭代;不稳定的测试会让 Agent(和人)学会忽略失败,甚至反复重试直到“碰巧通过”。应该把大量逻辑验证下沉到秒级的单元测试和静态检查,E2E 只保留少数核心流程,并修复不稳定的测试。