AI 辅助测试:用单元测试门禁兜住 AI 生成代码的底
Categories:
AI 辅助测试解决的不是测试覆盖率, 而是我对 AI 生成代码的信任问题. AI 三分钟能吐出一大段代码, 人却要两小时才检视得完, 三分钟生成, 两小时检视并不夸张. 我的解法是换一个检视对象: 不去逐行读代码, 改去读用例文档, 再用单元测试在本地开发环境建一道门禁, 让失败的用例替我盯住 AI 的偷懒和出错.
小 bug 才是 AI 辅助测试的主战场
bug 分为两类: 好解的 bug, 和不好解的 bug.
不好解的 bug 通常源于架构或方案设计缺陷, 逐步演化为历史包袱, 牵一发而动全身; 也可能由第三方引起, 如三方库, 操作系统, 编译器, 需要更专业的知识和深入探索才能解决. 这类复杂问题不在本文探讨范围.
本文关注的是产品生命周期里的小 bug, 借助 AI 辅助创建测试用例, 解决以下问题:
- 可以快速发现的 bug
- 只需极小改动就能修复的低级 bug
- 修改引入的新 bug
- 紧急修改时忙中出错
- 针对性测试的成本与效率权衡
大多数 bug 是小 bug, 但小 bug 也可能造成严重问题. 把小 bug 解决在本地, 能省下大量后续精力.
把检视对象从代码换成用例文档
我曾尝试过一个最大胆的想法: 让 AI 全权完成一个需求, 完全不检视它生成的代码, 类似 Copilot Agent 模式 那样彻底放手. 后来放弃了. 根本原因是心里没底. 对于一个逻辑严谨, 0 1 分明的二进制程序, 心里没底就意味着丢掉上下游的信任, 丢掉客户的信任. 所以我在学习对 vibe coding 的思考: 对 AI 生成的代码, 一定要心里有底.
问题在于检视成本. AI 有时生成或修改的代码量太大, 三分钟生成, 两小时检视并不夸张. 测试代码的工作量通常还多于业务代码, 且未必计入研发的交付考核, 这是很多人不愿主动写测试的原因之一. 但 AI 助手比我聪明很多 已是事实, 与其抗拒, 不如给它配一道门禁.
把检视对象从代码换成用例文档, 是绕开这个死结的关键. 与业务代码不同, AI 生成的测试用例通常描述一个非常具体的场景, 易于检视和验证. 很多用例甚至不必逐行读, 只等它报错时再细看. 说完全不检视 AI 代码是言过其实, 但借助完善的用例文档, 可以证明并非每行代码都需要人工 review.
---
title: 从检视代码到检视文档
---
flowchart TB
A[AI 生成大量代码] e1@--> B[逐行检视成本高]
B e2@--> C{检视什么}
C e3@-->|代码| D[两小时检视, 心里没底]
C e4@-->|用例文档| E[易于检视, 快速验证]
E e5@--> F[单元测试门禁]
F e6@--> G[失败用例暴露问题]
classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class D risk;
class E,F,G ok;
class e1,e2,e3,e4,e5,e6 animate;方法论底座: 六西格玛与 DMAIC
Six Sigma 是以数据驱动的质量改进方法论, 核心目标是通过减少过程变异, 把缺陷率控制在百万分之 3.4, 即 6σ 水平1.
它给出了一套可执行的流程 DMAIC:
- Define 定义: 需求分析, 转化为数据模型, 设计数据结构和接口
- Measure 测量: 用例监控, 覆盖率, 性能, 安全
- Analyze 分析: 分析用例和业务代码
- Improve 改进: 改进用例和业务代码
- Control 控制: 自动化测试门禁
配套的技术工具包括 linter, 静态检查, 动态检查和测试框架. 可关注的代码质量指标也很多, 比如复杂度指标(圈复杂度, 认知复杂度, 嵌套层级深度, 类耦合度), 质量指标(重复率, 函数长度, 魔法数字), 以及测试指标(单元测试覆盖率, 集成测试通过率, 用例有效性, 异常处理覆盖率).
方法论和工具能帮助发现和解决问题, 代价是可能一定程度上提升交付成本, 降低效率. 在有限资源下, 要选一种短中长期都能持续获益的方法. 我的选择是用单元测试作为开发过程中的辅助工具.
借助 AI 在不同角色语言间的转换, 开发可以融合 TDD, BDD, ATDD 的优势, 快速生成用例表和用例代码, 提升交付质量.
测试驱动: TDD/BDD/ATDD 与可测试代码
三种测试驱动的差异如下:
| 维度 | TDD | BDD | ATDD |
|---|---|---|---|
| 驱动来源 | 单元测试 | 用户行为场景 | 验收条件 |
| 参与者 | 开发者 | 业务+开发 | 业务+测试+开发 |
| 测试层级 | 单元测试 | 集成/端到端测试 | 验收测试 |
| 工具示例 | JUnit, pytest | Cucumber, SpecFlow | FitNesse, Robot Framework |
| 典型输出 | 代码覆盖率报告 | 可执行的用户场景文档 | 验收测试通过率 |
| 语言 | assert add(2,3) == 5 | 自然语言场景 (Given-When-Then) | 业务语言 (验收条件表格) |
可测试代码有四个核心原则: 单一职责(每个函数只做一件事), 依赖注入(通过参数传递依赖), 接口隔离(输入输出类型清晰), 状态可观测(提供查询接口验证内部状态).
给 AI 的提示词里, 我建议直接写 可扩展, 单一职责, 依赖注入. 我曾经尝试让 AI 控制生成的函数长度和文件长度, 它的遵循程度很差; 但换成单一职责和依赖注入后, 生成的代码明显更符合预期. 过早优化是万恶之源, 生成基础数据结构和接口时, 提示 AI 可扩展性优先即可.
方便测试的代码示例:
// user.h
typedef struct {
int id;
char name[50];
int age;
} User;
// 内存存储接口抽象
typedef struct {
User* (*get_user)(int id);
int (*save_user)(const User* user);
} UserStorage;
// 创建用户 (依赖注入存储接口)
User* create_user(const char* name, int age, UserStorage* storage) {
if(strlen(name) >= 50 || age <= 0) return NULL;
User* new_user = malloc(sizeof(User));
new_user->id = generate_user_id();
strncpy(new_user->name, name, 49);
new_user->name[49] = '\0';
new_user->age = age;
if(storage->save_user(new_user) != 0) {
free(new_user);
return NULL;
}
return new_user;
}
不便测试的代码示例:
// 紧耦合数据库操作
void save_user_to_db(User* user) {
MYSQL conn;
mysql_init(&conn);
if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
log_error("DB connection failed"); // 难以测试的日志依赖
exit(1); // 直接退出影响测试执行
}
// 拼接 SQL 并写入数据库
}
好的代码润物细无声, 不好的代码存在感强. 便于测试的函数返回值明确, 错误处理规范, 传入参数明确, 副作用隔离, 支持依赖注入.
AI 给出的代码有时不是最佳实践, 风格也有问题. 它虽然常常能解决问题, 但实际项目不是一锤子买卖, 必须考虑后续维护. 提示 AI 生成代码时, 同样要强调 可读性优先, 可扩展, 单一职责, 依赖注入. 代码风格问题不做更多讨论, 可参考 Google 的代码风格指南2.
实际开发的坑与权衡
需求在跨角色传递时, 会经历多轮语言转换, 每一轮都可能产生表述偏差. 例如客户说的"集中查看"被后端理解成"单接口返回"而没有做分页. 借助 AI 生成各角色语言模板, 可以减少人工转换的信息损耗.
思虑不周是很多 bug 的根源. 借助 AI 设计用例场景, 它不仅能完成预期任务, 还能超预期地给出启发, 补上发布指令者没考虑到的细节. 在每一轮角色语言转换中, 都可以和 AI 对话来完善漏洞. Markdown 是 AI 最容易理解的语言, 输入输出应尽量采用 Markdown 格式.
用例代码有几个必须注意的点: 用例隔离, 模拟依赖准确性, 断言有效性, 边界值, 异常处理, 性能, 并发. AI 会偷懒, 不会一次生成覆盖所有场景的用例. 一次生成全部测试代码对模型要求较高, 可以分多次生成.
以 Cursor 中的 Claude 模型 为例, 它对用例代码的部分注意事项完成较好, 但常忘记用例隔离和异常场景. 加一条 user rule 可以稍微减少偷懒: 生成测试用例时注意用例文档, 用例隔离. 不过它仍不会一次生成所有场景, 正常路径, 异常路径, 边界值, 性能, 并发等场景需要多次对话补充.
一个可行的 AI 辅助测试工作流
结合设计文档, 可以串起一条从场景到用例的流水线:
---
title: AI 辅助测试工作流
---
flowchart TB
A[设计文档] e1@--> B[生成验收测试场景]
B e2@--> C[生成数据结构和接口]
C e3@--> D[生成端侧用例表]
D e4@--> E[生成用例代码]
D e5@--> F[生成业务代码]
E e6@--> G[本地门禁: 用例通过]
F e7@--> G
G e8@--> H{存在失败用例}
H e9@-->|是| E
H e10@-->|否| I[提测]
classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A input;
class B,C,D,E,F gen;
class G,H,I gate;
class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;各步骤的提示词可以这样组织: 根据产品设计文档生成可验收测试场景; 根据测试场景生成数据结构和接口; 根据测试场景和接口生成端侧用例表; 根据接口和用例表生成用例代码; 根据测试场景生成业务代码并确保用例通过.
这里有一个隐患: AI 既可能根据测试代码改业务代码, 也可能反过来改测试代码. 如果两边都存在错误, AI 会迷惑, 随着对话次数增加, 测试代码和业务代码会越来越乱, 此时必须人工干预.
一个贪大的提示词是反例: 要求 AI 一次性覆盖正常, 异常, 边界值, 并发并生成代码. 这类请求容易中断. 正确做法是在 Cursor 里拆分场景, 用多个请求分批生成. 性能测试用例需要大量数据, 应放到系统测试阶段, 先让 AI 写随机数据生成器, 再用生成的数据构造用例.
集成测试需要构建测试环境. 有的用例依赖外部工具, 比如微隔离依赖 iptables 和 ipset, 可以用 Docker 构建测试容器. 原则是: 人怎么测, 就指挥 AI 实现相同的自动化步骤. 关于安全测试, 目前主要依赖经验和分析, 还没有成熟的 AI 辅助经验.
总结
这条工作流相比不使用 AI 会产出更多文档和测试代码, 很难做到每个字都检视. 但文档比代码更容易检视, 尤其是用例表, 它对用例生成有准确的指导作用. 实际开发中, 我有时只检视失败的用例, 通过调试失败用例来确认问题.
改进方向有三点: 设计阶段子流程有交叉, 验收测试场景可能依赖数据结构和接口; 端侧用例表由研发自己关注; 提测前需要再次生成包含业务代码细节的详细测试场景.
最终收益不在测试覆盖率数字, 而在信任: 借助单元测试这道本地门禁, 我对 AI 生成的代码终于能做到心里有底, 而不是把 vibing 出来的代码不假思索地送进生产.
Google Style Guide 是 Google 公开的代码风格指南. ↩︎