#Vibe Coding#TypeScript

用 AI 生成测试:实战

测试欠债是每个团队的痛,AI 生成测试能大幅缓解——但也能制造一堆无意义测试和虚假覆盖率。本文讲清 AI 生成测试的正确姿势:哪些测试适合交给 AI、生成→验证→接受的工作流、避免覆盖率陷阱、工具选型(Claude Code/Codium/自建),以及 AI 测试替代不了的部分。

本文要点

  • 主题聚焦:Vibe Coding、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年12月17日·更新于 2026年7月23日

每个团队都有测试欠债。原因高度一致:写测试比写代码慢、deadline 在头顶、重构一来测试先挂、补测试永远是"下一件事"。结果就是核心逻辑裸奔,线上出事才想起来补一个回归。

AI 生成测试看起来是这个问题的银弹——给它一个文件,十秒钟吐出 80% 覆盖率。但真用起来,你会撞上两类坑:一是它生成的测试什么都没断言,跑是过了,等于没测;二是它给你刷出一堆虚假覆盖率,行覆盖 100%,但一个 bug 都拦不住,还让 CI 变慢、重构变难。

AI 生成测试不是"一键还债",而是"把补测试这件体力活自动化"。体力活能自动,设计活不能。这篇文章讲清怎么把能自动的那部分做扎实,把不能自动的那部分留给人。

一、先分清:哪些测试 AI 能写好,哪些它写不好

让 AI 写测试之前,先判断这段被测代码属于哪一类。

AI 写得好的,基本都是"输入输出确定、依赖干净"的:

  • 纯函数:格式化器、解析器、校验器、金额计算、日期处理。签名清楚、无副作用、断言点明确,AI 几乎不会翻车。
  • 边界条件:null / undefined / 空数组 / 负数 / 超长字符串 / Unicode / 时区边界。这些是 AI 的强项——人懒得写的角落,它会一个个铺。
  • 参数化(表驱动)测试:给定一组 (input, expected),生成 it.each 的 case 列表。
  • 回归测试:你给它一个 bug 报告和复现步骤,它能把失败 case 固化成测试。这是性价比最高的用法之一。
  • 快照测试:序列化输出、组件渲染树、CLI 的 stdout。

AI 写不好的,基本都是"需要理解全局或业务意图"的:

  • 架构测试:模块边界、依赖方向、分层约束。这要它先理解整个系统,而它通常只看得到你贴的那个文件。
  • 业务语义测试:"这个优惠叠加规则对不对""这个结算流程和产品要的一致吗"——AI 不知道"对"长什么样,它只会按代码现状去描述代码现状,等于自我证明。
  • 探索性 / 性能测试:哪里可能慢、哪里会并发冲突、哪里有 race condition,这些要人去想,AI 只能照着显式分支机械覆盖。
  • 测试设计本身:该测什么、不该测什么、哪些路径风险高——这是测试最有价值的部分,也是最不能外包的部分。

一句话:AI 擅长补"广度",不擅长补"判断"。 把广度交给它,把判断留给自己。

二、工作流:不是一次生成,是"生成→跑通→修→接受"的循环

用 AI 写测试最常见的错误用法:贴一段代码,让它"写测试",然后把输出粘到文件里。这样得到的测试大概率跑不起来,或者跑起来了却在测错东西。

正确的姿势是一个闭环,核心原则只有一条:生成的测试必须能跑通,跑不通就让它自己修到通为止。

  1. 分析被测代码:先把签名、类型、依赖摸清楚。重点是什么是外部依赖(db、HTTP、文件系统、时间、随机数),这些必须被 mock;什么是内部协作对象,这些不该 mock。
  2. 生成:让 AI 基于签名和依赖列表给出测试用例,包括正常路径、边界、异常。
  3. 执行验证:立刻跑 vitest run / jest,把结果喂回 AI。
  4. 失败修复:测试挂了分两种——mock 配错了(改测试),或者暴露了真 bug(改实现,而且你赚到了)。AI 自己根据报错修,不要你转述。
  5. 人工接受:最后一关是你看一眼——这个测试到底在测什么行为,是不是换个错误实现它还能过?

Claude Code / Cursor 这类 agent 工具天然适合这个循环,因为它能自己跑命令、读报错、迭代。一个能直接用的 prompt 长这样:

text
为 src/pricing/calculate.ts 里的 calculateDiscount 生成 Vitest 单元测试。
要求:
1. 先读懂函数签名和返回类型,识别所有外部依赖并正确 mock。
2. 覆盖:正常折扣、满减边界、负数/0/空输入、叠加规则上限。
3. 每个断言必须验证具体业务期望,禁止 toBeDefined / toBeTruthy 这种空断言。
4. 生成后执行 `pnpm vitest run src/pricing/calculate.test.ts`,
   有失败就自己修,直到全部通过。把最终的覆盖率贴给我。

关键在最后一条:让它自验证。Agent 能读错误自我纠错,比你来回粘贴快一个数量级。如果你用的工具不能跑命令(Copilot 式补全),那它生成的测试你必须手动跑一遍,别偷懒——跑不起来的测试比没有测试更糟,它会腐烂、给假信心、还挡 CI。

判断一个生成测试有没有价值,有个很狠的办法:变异测试(mutation testing)。Stryker 这类工具会故意把你的实现改错(> 改 >=、+ 改 -),然后看测试能不能挂。如果代码被改错了测试还过,说明这个测试形同虚设。AI 生成的测试尤其值得过一遍 mutation,它能瞬间暴露"为了覆盖率而写"的废测试。

三、质量把控:别让覆盖率变成陷阱

覆盖率是最容易被 AI 玩坏的指标。下面这个测试,覆盖率工具会给它打满分:

ts
// 看起来测了,其实什么都没测
describe("formatPrice", () => {
  it("works", () => {
    const result = formatPrice(99);
    expect(result).toBeDefined();
    expect(typeof result).toBe("string");
  });
});

行覆盖 100%,但 formatPrice 哪天把 ¥99 改成 ¥0,这个测试照样过。它没断言任何业务期望,只是证明了"函数返回了个东西"。

好测试的标准只有一个:它能不能拦住一个合理的错误实现。 据此有几条硬规矩:

  • 断言要落在业务期望上,不是落在"它返回了"。 expect(formatPrice(99)).toBe("¥99.00") 是好断言;expect(result).toBeTruthy() 是废断言。AI 很爱生成后者,审稿时一律删。
  • 测行为,不测实现。 不要为了覆盖私有函数的某个分支去测内部方法,也不要把内部调用 mock 死去凑分支覆盖。实现一重构测试就全挂,这种测试不是资产是负债。
  • 拒绝重复测试。 同一个分支三个 case 都在测同一件事,只留一个最有代表性的,其余删掉。AI 会为了"看起来努力"生成大量同质 case。
  • 覆盖率是副产品,不是目标。 好的测试自然会带来合理覆盖;为了覆盖率去写测试,得到的是一堆凑行数的废物。把覆盖率当门禁可以(防止明显漏测),当 KPI 必反噬。

一个实用原则:审 AI 生成的测试,比审它生成的实现还重要。 因为错的实现会被测试拦住(如果你测试写对了),错的测试没人拦,它会一路绿灯混进主干。

四、Mock 与依赖:AI 最容易翻车的地方

AI 生成测试翻车,十次有八次翻在 mock 上。典型症状:

  • mock 太深:把内部模块、内部函数全 mock 掉,被测代码只剩一层 if-else,等于在测 mock 不是测代码。
  • mock 太浅:该 mock 的数据库没 mock,测试真的去连了开发库,时快时慢还污染数据。
  • 忘了 reset:上个测试的 mock 状态漏到下个测试,出现"单独跑过、一起跑挂"的玄学失败。
  • mock 错了层:该在 repository 层 mock 的,跑到 fetch 层 mock,结果换了个 HTTP 库测试全废。

正确的 mock 原则:只在边界处 mock——数据库客户端、HTTP 客户端、外部 API、时间、随机数、文件系统。 内部协作对象用真实实现。这样测的是"真实代码 + 假外部世界",而不是"假代码 + 假世界"。

ts
import { describe, it, expect, vi, beforeEach } from "vitest";

// 正确:在边界(发邮件)mock,内部逻辑用真实代码
vi.mock("../email/client", () => ({
  sendEmail: vi.fn().mockResolvedValue({ ok: true }),
}));

import { sendEmail } from "../email/client";
import { checkout } from "./checkout";

beforeEach(() => vi.clearAllMocks()); // 关键:每个 case 重置

describe("checkout", () => {
  it("下单成功后发送一封确认邮件", async () => {
    const order = await checkout({ userId: "u1", items: ["sku1"] });

    expect(order.status).toBe("confirmed");
    expect(sendEmail).toHaveBeenCalledOnce();
    expect(sendEmail).toHaveBeenCalledWith(
      expect.objectContaining({ to: expect.any(String), subject: "订单确认" }),
    );
  });
});

两个细节别漏:一是 beforeEach(() => vi.clearAllMocks()),不写就会出现跨用例污染;二是别 mock 你没拥有的东西——第三方库的内部方法别 mock,要在自己包一层适配器再 mock 那层适配器,否则库一升级测试全挂。

还有两个隐形依赖:时间和随机。Date.now()、new Date()、Math.random()、crypto.randomUUID(),只要被测代码碰了,测试就会"今天过明天挂"。Vitest 用 vi.useFakeTimers() 锁时间,ID 生成用注入或 vi.spyOn 固定。AI 经常漏掉这些,要 explicitly 提醒。

五、工具选型:Claude Code、Cursor、Codium、自建

不同工具适合不同场景,别指望一个打天下。

  • Claude Code / Cursor(agent 模式):适合"生成 + 自验证"闭环。它们能自己跑测试、读报错、迭代到通过。首选场景:从零给一个模块补测试。 给它文件路径 + 测试要求 + 让它自己跑通,你能腾出手干别的。关键是给清楚约束(用什么框架、mock 哪一层、禁止哪种断言),否则它会按默认习惯写。
  • CodiumAI / Qodo Cover:专门做测试生成的工具,强项是场景穷举——它会主动问你"这条异常分支测了吗""这个 null 输入考虑了吗",对"我漏了什么"特别有用。适合给老代码补行为测试,但生成的 mock 有时很啰嗦,需要精简。
  • Diffblue(Coverage Advisor,Java 为主):字节码层面分析,适合企业级 Java 仓库批量补测,TS 项目用不上,但思路值得借鉴——它强调"基于真实执行路径生成"。
  • 自建 agent:团队有统一测试约定(固定 mock 工厂、固定 fixture、固定目录结构)时最划算。用 Claude API + tool use 自己跑测试,把项目规范、mock 模板、禁止项塞进 system prompt,生成的测试直接符合团队规范,几乎不用人工调整。前期投入大,规模化后回报最高。

选型其实就一句话:要闭环用 agent,要场景穷举用 Codium,要规模化统一规范就自建。 大多数团队前两个就够,自建是规模化之后的事。

六、AI 替代不了的那部分:测试设计

说了这么多 AI 能做的,得把边界划清楚——AI 能补充覆盖,替代不了"测试设计"本身。

测试真正有价值的部分,从来不是那几百行 it(...)。是这些:

  • 该测什么、不该测什么:资源有限,测哪些路径收益最高?AI 不知道哪些是核心资产、哪些是一次性脚本。
  • 风险在哪里:这个函数错了会让公司赔钱还是会让 UI 错位?优先级是业务判断,AI 看不出来。
  • "对"长什么样:业务规则的对错,在产品文档和用户脑子里,不在代码里。AI 只能复述代码现状。
  • 设计反馈:写一个函数时发现"这玩意儿根本没法测",往往说明设计有问题——依赖太重、职责太杂、副作用太深。这种"测试驱动设计"的洞察,是人在写测试时最大的副产品,AI 替你写了,这个信号就丢了。

所以最健康的用法是:AI 负责把已经设计好的测试铺满,人负责设计测试。 你决定测哪些行为、哪些边界重要、哪些风险不可接受;AI 负责把这些决定快速落地成可执行的测试代码,并且帮你发现"哦这条分支你忘了测"。它是个不知疲倦的执行者,不是测试架构师。

小结

  • 先分类再动手:纯函数/边界/参数化/回归适合交给 AI,架构/业务语义/探索性/测试设计不合适。把广度给 AI,把判断留给自己。
  • 必须闭环:生成的测试要能跑通,让 agent 自己执行、读报错、修到通过。跑不起来的测试比没有测试更糟。
  • 严控质量:拒绝空断言、拒绝重复、拒绝为覆盖率而测。审 AI 的测试要像审它的代码一样严,因为错的测试没人拦。
  • mock 只在边界:数据库/HTTP/时间/随机这些外部依赖 mock 掉,内部协作对象用真实实现。别忘了 reset 和 fake timer。
  • 覆盖率是结果不是目标:它好测试的自然副产品。需要门禁,但绝不能当 KPI。

AI 能让测试欠债从"永远还不上"变成"还得起",但它还的是体力债,不是脑力债。决定测什么的那个决定,永远得你来下。

Related

相关推荐