CHAPTER 10 · 全栈

工程化与质量保障

当代码大部分由 Agent 编写,你的核心工作之一就变成了建立一张自动化的安全网:让错误在几秒钟内、以明确的方式暴露出来,而不是靠你逐行阅读。这张网同时也是 Agent 的“眼睛”——它靠测试结果知道自己写对了没有。

⏱ 约 50 分钟会用(类型检查、Vitest、Playwright、CI)知道(Monorepo、可观测性工具)
读完本章,你应该能够:
  • 说出验证金字塔每一层能抓住什么错误、代价是多少。
  • 写出一个 Vitest 单元测试和一个 Playwright 端到端测试。
  • 配置一个在每个 PR 上运行类型检查、Lint、测试和构建的 CI 流水线。
  • 识别 Agent 在测试上的典型“作弊”行为。

开场:你不可能读完所有代码

开场问题

Agent 一天之内为 LinkNote 提交了 30 次修改,涉及 80 个文件。你认真审查了其中最重要的 5 个。凭什么相信另外 75 个文件没有把已有功能改坏?

答案方向:你不需要相信 Agent,你需要相信检查。把“什么叫做正确”编码成机器可以执行的检查(类型、规则、测试),每次修改都自动运行。然后你的审查重点就从“这 80 个文件对不对”转移到“这些检查是否覆盖了重要的行为”和“这 5 个关键设计决策对不对”。

验证金字塔

人工验收 端到端测试 E2E(Playwright) 集成测试(API + 真实数据库) 单元测试(Vitest) 静态检查:类型(tsc)+ Lint + 格式化 慢 · 贵 · 少 快 · 便宜 · 多 预览链接里点一点 关键用户流程能走通 SQL、鉴权、接口契约 纯逻辑、边界条件 拼写、类型、 危险写法
越往下,运行越快、越便宜,应该越多;越往上,越接近真实用户,但越慢、越脆弱,只覆盖最关键的流程。

对于 Agent 工作流,最底下两层的价值被放大了:它们在几秒钟内给出结果,Agent 可以在一个循环里反复“修改 → 检查 → 修复”,而不需要你介入。

静态检查

类型检查

第 01、04 章已经强调过:tsc --noEmit 必须零错误,并且在 CI 中强制执行。TypeScript 7 之后,大项目的类型检查也只需要几秒钟。

Lint 与格式化

两种主流组合:

值得开启的规则都有一个共同点:它们针对的是“能运行但有隐患”的代码——正是 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 时代的测试策略

  1. 测试就是规格。对于重要功能,先让 Agent 写测试(或者你写测试的标题列表),确认测试表达了你要的行为,再让它实现。这就是测试驱动开发(TDD),在 Agent 工作流里它的价值比以往更高:测试把模糊的需求变成了可执行的验收标准。
  2. 警惕“改测试让它通过”。Agent 在修不好代码时,有时会修改测试的断言、加上 .skip、或者把期望值改成当前的错误输出。审查 diff 时,对测试文件的修改要格外仔细看。可以在 AGENTS.md 里写明:“除非我要求,不得修改已有测试的断言”。
  3. 小心过度 mock。把数据库、网络全都 mock 掉的测试,测的往往只是 mock 本身。涉及 SQL、鉴权的逻辑,用真实数据库(本地 Docker Postgres、测试用 D1、或 PGlite 这类内存 Postgres)写集成测试更有价值。
  4. 快照测试少用。大块的快照很容易被“一键更新”,失去检验意义。

CI:把检查变成门禁 会用

然后在 GitHub 仓库设置里开启分支保护:主分支只能通过 PR 合并,且 CI 必须通过。这样,不论是你还是 Agent,都无法把没通过检查的代码合进主分支。E2E 测试可以在部署平台生成预览 URL 后,针对预览环境运行。

Git 习惯

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 章
对 Agent 怎么说:写一份“完成的定义”

把下面这样的“完成定义(Definition of Done)”放进 AGENTS.md,每个任务都适用:

模糊
精确
“修复搜索 bug。”
“搜索在标题为空时崩溃。先写一个复现这个问题的失败测试(标题为 null、按 URL 搜索),确认它失败;然后修复;最后运行完整的测试套件。”
面试角
  • 单元测试、集成测试、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 只保留少数核心流程,并修复不稳定的测试。