我是怎样使用 AI 构建 E2E 测试体系的?
TinyShip 是一个同时支持 Next.js、Nuxt.js、TanStack Start 三套前端框架,以及 PostgreSQL 和 SQLite 两套数据库的 monorepo 项目。每改一个功能,就有 6 种组合可能出错,光靠人工测试根本忙不过来。作者总结了一套 Spec → Code → Verify(agent-browser)→ Test → Green 五阶段流程,让 AI 辅助完成 E2E 测试的整个生命周期,极大提升了开发信心和交付质量。
问题
TinyShip 是一个支持 Next.js、Nuxt.js、TanStack Start 三套前端框架的 monorepo,同时兼容 PostgreSQL 和 SQLite。这意味着每改动一个功能,就有 6 种不同的组合可能出问题。当开发新功能时,确保应用功能正常且没有回归(regression)变得至关重要。如果全靠手动测试,工作量会大到难以想象。我在开发基础功能后,发现添加后续功能时如果没有 E2E 测试,几乎寸步难行。尤其是面对一个多框架、多数据库支持的应用,流程相似、重复性极高,因此必须有一套在新功能开发时就能同步测试与验收的流程。
基石
TinyShip 的基石是 E2E 测试。我认为在 AI 编码时代,任何产品的基石都是测试,用户用例(User Cases)比代码更宝贵。AI 让代码迭代速度从“天”变成了“小时”,你可能一天重构 10 次、添加 20 个功能,每次改动都可能意外破坏现有功能。
虽然 E2E 测试看起来有些重,但我还是毅然把它加上了。事实证明,有了 AI 的辅助,任何看似繁琐的任务实施起来都不难。我让 AI 通过路由和页面分析核心交互,确定必须覆盖的关键流程(Critical User Journeys),然后编写用例,加上我修改和 review,总共花了不过两天时间。这些测试模拟真实用户在浏览器中的完整操作,保证核心流程 100% 有 E2E 覆盖。这样一来,任何修改提交和后续开发都有了重要依靠,对后续功能开发至关重要。
五阶段流程
有了 E2E 覆盖,我确定了一套开发新功能的新流程。TinyShip 在开发新功能时遵循五个阶段:Spec → Code → Verify → Test → Green。我将这套标准写到了根目录的 Agents.md 文件中,这样 AI 可以第一时间按照我的流程完成功能。
核心思路是:先想清楚要测什么,再写代码,然后用 agent-browser 走一遍视觉确认,最后写 Playwright 测试。顺序很重要。这套流程在 Agent 的 Plan 模式下就会被激活,伴随着技术方案的创建,在开始 build 以后会完成接下来的步骤。
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
│ SPEC │──▶│ CODE │──▶│ VERIFY │──▶│ TEST │──▶│ GREEN │
│ 定义验收 │ │ 实现功能 │ │ 视觉确认 │ │ 写 E2E │ │ 全通过 │
│ 标准 │ │ │ │ │ │ 测试 │ │ │
└─────────┘ └─────────┘ └──────────┘ └─────────┘ └─────────┘
Spec:先想清楚要测什么
每做一个新功能,第一步是让 AI 在 tests/e2e/TEST-CATALOG.md 里写一段验收标准。就是用自然语言描述:打开哪个页面、点哪里、期望看到什么。例如下面这个已有的用例,除了自然语言描述,还可以增加结构化字段。
## 8. 个人资料更新测试
**文件:** `specs/profile-update.spec.ts` | **优先级:** P1
验证仪表盘中编辑个人资料的完整流程:进入编辑模式 → 修改姓名 → 保存 → 验证更新。
> 所有测试共用一个浏览器上下文(`beforeAll` 注册),按串行顺序执行。
| # | 测试名称 | 具体流程 |
|---|---------|---------|
| 1 | 个人资料标签页显示用户名和编辑按钮 | API 注册用户 → 访问 `/dashboard` → 验证用户名可见 → 验证 "Edit" 按钮可见 |
| 2 | 可以进入编辑模式并修改姓名 | 访问 `/dashboard` → 等待用户名加载 → 点击 "Edit" 按钮 → 验证 `#name` 输入框可见 → 清空并填入新姓名 → 点击 "Save" → 等待编辑模式关闭("Edit" 按钮重新出现) → 验证新姓名显示在页面上 |
Code:写代码
这个没什么好说,按清单写代码。但写的时候要注意一点:保持三个 app 的一致性。互相复用的逻辑放在 libs/* 里实现,路由层尽量薄。这样 E2E 测试写起来也省事,三个 app 的测试逻辑基本一样。
Verify:用 agent-browser 预演一遍
代码写完了,页面跑起来了,接下来不是写测试,而是先用 agent-browser 走一遍。agent-browser 是 Vercel Labs 专门为 AI Agents 设计的浏览器自动化 CLI。
为什么要使用 agent-browser ?
为什么多这一步?
本文后半部分聚焦于 Verifiy 阶段的 agent-browser 工具如何大幅节省 Token,以及 Test、Green 阶段的实操细节。作者解释了为什么 E2E 测试不在 CI 上跑,并分享了在 TinyShip 这种三框架、两数据库(共 6 种组合)项目中,如何用 AI 辅助写出稳定选择器并高效跑通所有组合。
为什么 Playwright 测试很脆弱
Playwright 测试有一个常见痛点:选择器需要反复调整。如果 UI 存在明显的交互问题,写测试也是白费功夫,后续还得改。另外,多次运行测试非常耗时,而且会浪费不少 Token。
agent-browser:基于 Accessibility Tree 的智能交互
为了解决上述问题,我使用了 agent-browser,它基于 Rust 和 Playwright 底层构建,最大的优势是极致节省上下文和 Token。
传统的 Playwright 或 Puppeteer 给 AI 喂一页 HTML/DOM 树,动不动就是几千到上万个 Token,很快就撑满上下文。而 agent-browser 采用语义化的 Accessibility Tree(无障碍树)加上简洁的引用(例如 @E_1、@E_3 - button "生成图片"),输出非常紧凑,能节省 80% 以上的 Token。它还是 AI-First 设计,用自然语言指令就能很好地理解你的意图。
下面是一个 agent-browser 返回的例子,只保留交互元素,没有整棵 DOM 树,效果非常直观:
- textbox "输入提示词" [ref=e1]
- button "选择文件" [ref=e2] (上传按钮)
- combobox "模型选择" [ref=e3] (下拉框)
- button "开始生成" [ref=e4]
交互时直接使用上面的 ref 即可,完全不用写 CSS 选择器:
agent-browser click @e4
Verify 阶段:先拿到可靠选择器再写 Playwright 测试
在 Verify 阶段,用 agent-browser 走完真实流程后,我们已经获取了可靠的元素引用和实际 DOM 结构。此时再写 Playwright 测试的选择器,成功率极高,基本一次就能稳定。而且三个框架(Next.js、Nuxt.js、TanStack Start)的测试代码也可以高度复用,只需少量调整。
Test:什么时候写 Playwright 测试
等 UI 界面确认没问题之后,才开始写 Playwright 测试。这时候选择器都已经知道了——哪个按钮是 [data-slot="select-trigger"],哪个列表是 role="listbox",哪个输入框的 placeholder 是什么。
为什么不提前写好测试?
有人会问:BDD 不就是先写测试吗?
试过,不行。
E2E 测试跟单元测试截然不同。单元测试针对的是一个函数,输入输出都是纯数据,可以在写代码之前先写测试。但 E2E 测试依赖真实的 DOM 结构——比如 [data-slot="select-trigger"] 这种选择器,你根本不知道 UI 会长什么样。而且三个框架的渲染方式不同,同一个选择器在一个框架里有效,在另一个里可能失效。
所以我的做法是:用 BDD 的思维先想清楚验收标准,但测试代码放在 UI 成型之后再写。
Green:六个组合全跑通
最后一步,启动 Next.js app,跑一遍测试。然后换成 Nuxt.js,再跑一遍。再换成 TanStack Start,再跑一遍。三个应用都通过后,切换数据库——从 PostgreSQL 换到 SQLite——再跑三遍。总共 6 次测试都通过,这个功能才算完成。
切换应用和数据库的工作交给 AI 自动完成,不需要手动干预。一个 app 跑完,自动切换下一个,运行相同的测试集。
E2E 测试不在 CI 上跑
E2E 测试有一个设计原则:我不让它在 CI 上跑。
CI 上只跑 typecheck 和 build。原因如下:
- 慢。全量 E2E 跑完一个应用大概 6 分钟,三个应用就是 18 分钟,再加上两个数据库,总共 36 分钟。CI 上排队这么久不划算。
- 依赖多。支付相关的测试需要 Stripe CLI 以及不同支付平台的各种环境变量,配置非常繁琐,要么麻烦要么不安全。
- CI 的目的不同。CI 应当提供快速反馈——类型对不对、能不能编译。E2E 解决的是另一个问题:交互流程有没有被破坏。两者不是一回事。
所以 E2E 现在只在本地跑,每次发版前,三个应用各跑一遍。
三种需要跑 E2E 的场景
E2E 不是天天跑全量的。我只在这几种情况下跑:
- 小修小补:只跑 typecheck + build 就够了。
- 发版前或重大重构:才需要跑全量 E2E。
如果你也维护多框架的项目,或者被人肉测试的高成本所困扰,不妨试试这套流程。