Home
avatar

.Sam

【AI Native开发-11】自动化用例:从文本用例到测试代码

摘要

文本用例定了”测哪些场景”,这一篇解决”怎么用代码把它们测起来”。让 AI 把 Given/When/Then 翻译成可运行的测试代码,看似一句话的事,实际是自动化测试最容易翻车的环节——AI 会新造一套 fixture、断言无关紧要的实现细节、写出永远通过的空测试。

核心论点:

  1. 先定测试层级,再写代码。 一条用例该用单元、接口还是 E2E 测,取决于它要验证什么。层级选错,要么测试又慢又脆,要么根本没测到行为。
  2. 复用项目现有测试设施,禁止 AI 另起炉灶。 fixture、登录态、测试工厂、断言工具——这些必须复用项目里已有的。AI 默认会新造一套能跑的,但那套和真实环境脱节。
  3. 断言业务不变量,不绑定实现细节。 好的自动化断言”用户能观察到什么 / 数据变成什么”,而不是”某个内部函数被调用了几次”。后者一重构就红,测试变成维护负担。

本篇给出测试层级选择、测试映射表、fixture 复用纪律、断言原则、数据隔离与幂等处理,以及自动化用例的准出标准。

一个典型场景

你让 AI”把删除功能的测试用例自动化了”。它很快写完,pnpm test 全绿。你很高兴,直到发现:

  • 它没有用项目现有的测试登录工具,自己在每个测试里手写了一个假的 session 对象,mock 掉了整个认证中间件——于是权限校验那段代码在测试里根本没被执行,TEST-002-4(绕过前端直打接口也要被拦)名义上自动化了,实际没测;
  • 它断言的是 expect(deleteArticle).toHaveBeenCalled()——验证”函数被调用了”,而不是”文件真的被删了 / 没权限时文件还在”;
  • 它在测试里真的删了 src/content/blog/ 下的一篇真实文章,第二次跑测试就因为文章不存在而失败;
  • 一个 E2E 用例里,它断言”确认弹窗关闭了”,但没验证取消时到底发没发删除请求。

全绿是假象。这些测试要么没测到行为,要么不可重复,要么断言错了对象。问题不在 AI 不会写测试语法,而在它不知道这个项目”怎么测才算数”。 这些规则必须显式告诉它。

核心问题:自动化阶段的三个默认错误

AI 写测试代码时有三个强烈的默认倾向,每一个都会让测试失效:

默认一:过度 mock,把要测的东西也 mock 掉。 AI 倾向于 mock 掉所有依赖让测试”干净”,但常常连被测逻辑本身都 mock 了——比如 mock 掉权限中间件,权限测试就成了空转。

默认二:断言实现而非行为。 AI 喜欢断言”某函数被调用""某变量等于某值”,因为这好写。但这些断言和内部实现绑死,重构即红,且不能证明用户看到的结果正确。

默认三:测试不可重复。 AI 会用真实数据、不留清理、依赖执行顺序。测试第一次绿、第二次红,或者反过来——这比没测试更糟,因为它制造了”已覆盖”的幻觉。

对策不是别让 AI 写测试,而是在它动笔前,先把层级、设施、断言原则、数据策略钉死

第一步:先选测试层级

对每条文本用例,先决定在哪一层测。不要所有东西都堆到 E2E(慢、脆),也不要全用单元(测不到真实协作)。

选择原则:问"这条用例验证的是哪一层的契约?"

单元测试(快、多、隔离):
  - 验证纯逻辑:状态判断、权限判断、校验函数
  - 例:hide: true 可删 / hide: false 不可删的判定逻辑
  - TEST-003 状态边界的核心判定

接口 / 集成测试(中速、真实协作):
  - 验证真实 HTTP 路由 + 真实中间件 + 真实文件操作
  - 权限中间件【不 mock】,用真实登录态
  - 例:TEST-002 权限、TEST-005 服务端二次校验、TEST-001 完整删除
  - 文件操作用临时目录,不碰 src/content

E2E 测试(慢、少、关键路径):
  - 验证用户真实操作路径,跨前后端
  - 只覆盖关键用户旅程和"前端行为"类断言
  - 例:TEST-004 取消确认不发请求(必须在浏览器层才测得到)

关键判断:TEST-002-4(绕过前端直打接口必须被服务端拦截)只能在接口层测,且权限中间件绝不能 mock。 前面那个事故里 AI mock 掉认证中间件,就是把这条用例的核心给架空了。选层级时就要明确”哪些依赖必须真实”。

第二步:先出测试映射表,再写代码

本文的接口/E2E 路径是可迁移的假想全栈项目示例,不是本博客当前仓库已经存在的文件或命令;落地前必须先核对真实测试设施。

不要直接让 AI 写测试代码。先让它产出一张”用例 → 自动化”的映射表,人确认后再写:

TEST 编号     层级    文件                        关键断言                数据准备
TEST-002-1   接口    tests/api/delete.test.ts    200 + 文件消失          临时未公开文章(hide:true) + admin
TEST-002-2   接口    tests/api/delete.test.ts    403 + 文件仍在(副作用)   临时未公开文章 + 普通用户
TEST-002-4   接口    tests/api/delete.test.ts    直打接口 403            普通用户,不经过前端
TEST-003-1   接口    tests/api/delete.test.ts    409 + 文件仍在           临时已公开文章(hide:false) + admin
TEST-004-1   E2E     e2e/delete-cancel.spec.ts   无 DELETE 请求发出       临时未公开文章 + admin
TEST-007-1   接口    tests/api/delete.test.ts    第二次返回 404            临时未公开文章 + admin

这张表让人能在写代码前就发现问题:某条 TEST 没分配层级?漏了。关键副作用断言没列?补上。确认映射完整再让 AI 生成代码,能避免”写完才发现没测权限”的返工。

第三步:复用现有测试设施

在指令里明确要求 AI 复用项目已有设施,禁止新造:

写测试前先做调研(只读):
1. 项目用什么测试框架?命令是什么?(看 package.json)
2. 现有的测试登录 / 认证 fixture 在哪个文件?怎么用?
3. 有没有测试数据工厂 / builder?
4. 接口测试怎么起服务、怎么发请求?
5. 临时文件 / 目录怎么创建和清理?(beforeEach/afterEach)

要求:
- 登录态【必须】复用现有 auth fixture,不要自己 mock 认证中间件
- 测试数据【必须】在临时目录创建(如 tmp/ 或 os.tmpdir),
  绝不能用 src/content 下的真实文章
- 每个测试结束后清理自己创建的数据(afterEach)
- 断言工具、请求工具复用现有的
- 如果现有设施缺东西(比如没有普通用户登录的 fixture),
  先提出来,不要默默 mock 掉

最后一条很重要:当现有测试设施不足以覆盖某场景时(比如没有”普通用户”的登录方式),正确做法是先补 fixture,而不是 mock 掉认证。 mock 认证是权限测试的头号杀手。

第四步:断言业务不变量

这是区分”有效测试”和”陪跑测试”的核心。对比两种写法:

import { vi } from "vitest"

// ❌ 断言实现:验证函数被调用,不验证结果
const deleteArticle = vi.fn().mockResolvedValue(undefined)
await deleteArticle(articleId)
expect(deleteArticle).toHaveBeenCalledWith(articleId)
// 即使被测逻辑什么都没做,这个 mock 测试也可能通过

// ✅ 断言行为:验证可观察的结果
const res = await fetch(`/api/articles/${article.id}`, { method: "DELETE" })
expect(res.status).toBe(200)

// 关键:验证副作用真的发生了
const after = await fetch(`/api/articles/${article.id}`)
expect(after.status).toBe(404)            // 文件确实没了

权限被拒的用例,副作用断言尤其关键(第 02 篇也强调过):

// ❌ 只断言状态码
const res = await fetch(`/api/articles/${article.id}`, { method: "DELETE" })
expect(res.status).toBe(403)
// 但如果代码在权限校验之前就把文件删了呢?状态码 403 掩盖了数据已丢

// ✅ 断言"什么都没发生"
expect(res.status).toBe(403)
const stillThere = await fetch(`/api/articles/${article.id}`)
expect(stillThere.status).toBe(200)       // 文件还在
// 副作用断言:拒绝操作不能留下任何痕迹

E2E 层的”取消不发请求”用监听实现(第 02 篇的例子):

let deleteCalled = false
page.on("request", (req) => {
  if (req.method() === "DELETE") deleteCalled = true
})

await page.getByRole("button", { name: "删除" }).click()
await page.getByRole("button", { name: "取消" }).click()

// 断言"什么都没发生",比断言"弹窗关了"更接近 Spec 原意
expect(deleteCalled).toBe(false)

断言原则总结:

断言用户 / 系统可观察的结果:
  ✓ 返回的状态码、响应体
  ✓ 数据的最终状态(文件在不在、记录变没变)
  ✓ 副作用(请求有没有发出、关联数据动没动)
  ✓ 界面可见的变化

不要断言内部实现:
  ✗ 某个内部函数被调用了几次
  ✗ 某个私有变量的值
  ✗ 弹窗组件的内部 state(除非这就是要测的行为)

判据:如果一次合理的重构(不改行为)会让测试变红,那这个测试断言的是实现,不是行为。

第五步:数据隔离与幂等

AI 写的测试最常见的不可重复问题,根源在数据。规则:

数据隔离:
- 每个测试自己造数据(用工厂 / builder),不依赖共享的固定数据
- 数据造在临时位置,绝不动 src/content 等真实内容目录
- beforeEach 准备,afterEach 清理,保证测试可独立、可乱序、可重复运行

幂等验证:
- 重复操作的用例(TEST-007),第二次明确返回 404(文章不存在)
- 断言第二次请求不会产生额外副作用,且系统状态一致

独立性:
- 测试之间不共享可变状态
- 一个测试的失败不影响其他测试运行

空实现测试:自动化用例的有效性自检

每条自动化用例写完,用第 01 篇的空实现法验证它”真的在测”:

把被测逻辑临时换成只返回成功的空实现(比如删除接口直接 return 200,
不做任何校验和文件操作),然后跑测试:
- 如果测试【变红】→ 测试有效,它确实依赖真实逻辑
- 如果测试【仍然通过】→ 测试无效,它没在验证任何东西
  (典型:mock 掉了被测逻辑,或断言和行为无关)

这个手法成本极低(改几行、跑一次、再改回来),但能抓出最隐蔽的”陪跑测试”。全绿但空实现也绿的测试套件,是负资产——它给你信心却不提供任何保护。

自动化用例准入准出

准入:

  • 文本用例已过评审,REQ → TEST 矩阵完整;
  • 测试映射表(TEST → 层级 → 文件 → 断言 → 数据)已经人确认;
  • AI 已调研并复用现有测试设施。

准出:

  • 每条文本用例都有对应自动化(或明确标注”仅人工验证”并说明原因);
  • 测试可独立、重复、乱序运行,不依赖真实内容数据;
  • 权限中间件等关键依赖未被错误 mock;
  • 断言的是行为和副作用,不是内部实现;
  • 权限 / 异常用例包含”副作用没发生”的断言;
  • 空实现测试法验证过关键用例确实有效;
  • 每条自动化用例都能通过 TEST 编号回溯到文本用例和 REQ;
  • 测试通过、失败信息可定位(失败时能看懂是哪条行为不对)。

自动化反模式

反模式一:全 mock 套件。 把所有依赖(包括被测逻辑、认证、数据层)都 mock,测试只验证”mock 之间按预期互动”。这种测试永远绿、毫无价值。对策:关键依赖必须真实,用空实现法检验。

反模式二:真实数据测试。 在真实内容目录上测删除,第一次跑删了数据,第二次就崩。对策:临时目录 + 工厂造数 + 强制清理。

反模式三:断言实现细节。 断言函数调用次数、内部状态,重构即红,团队逐渐不敢重构、甚至开始忽略测试。对策:断言可观察行为。

反模式四:跳过映射表直接写。 AI 拿到文本用例直接写代码,写完发现层级选错、漏了场景、mock 了不该 mock 的。对策:先映射后编码,人在映射表上把关。

失败回退

  • 自动化发现现有测试设施不足以覆盖场景(缺普通用户登录方式等)→ 先补 fixture,回到实现 / 测试基建
  • 写测试时发现文本用例的预期和实际合理行为冲突 → 回到测试设计核对 Spec;
  • 发现某条行为无法自动化验证(如纯视觉效果)→ 标注为人工验证项,进 Verify 的人工验收清单;
  • 测试红了 → 进入第 12 篇的调试流程,不在这里直接”修到绿”。

人审重点

  • AI 负责:调研测试设施、按映射表生成代码、造测试数据;
  • 人负责:确认测试层级选得对不对、关键依赖有没有被错误 mock、断言是否落在行为上、空实现自检的结果、数据隔离是否到位;
  • 映射表和空实现自检结果,是人评审自动化用例的两个主要抓手。

结语

自动化测试的目标不是”代码覆盖率高”,而是”当行为错了,测试会红”。一个空实现也能通过的测试套件,覆盖率 100% 也只是更贵的安慰剂。


测试写好了,一定会有红的时候。红了之后最危险的做法,是让 AI”看着办,修到通过”。下一篇讲测试调试:失败后怎么先保住证据、怎么区分这到底是产品 bug、测试 bug、环境问题还是数据问题、怎么让 AI 基于证据定位而不是靠猜,以及怎么把每次失败沉淀成回归资产,而不是修完就忘。

AI Native SDD 自动化测试 测试代码 测试工程