重新思考 AI 开发的单元测试:从正确性到合约保护
I’m happy to translate the article for you, but I need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line, formatting, and any code blocks exactly as they are.
Source: …
测试 AI 生成代码的悖论
当 AI 为你编写代码时,传统的单元测试假设会失效。
- 在常规开发中,我们先编写测试(TDD),因为人会犯错。
测试充当合同——实现必须满足的规范。 - AI 并不会犯同样的错误。AI 在类或方法层面的生成代码通常是正确的。
当我对 AI 编写的代码运行细粒度单元测试时,它们几乎总是一次就通过。
那为什么还要费心?
问题不在于正确性,而在于 变更检测。
当 AI 重构你的代码库时,它能出色地保持内部一致性,但可能会在你未明确标记的边界悄然破坏合同:
- 内部类接口发生变化。
- 命名空间的公共表面发生转移。
- 代码仍然能够编译,逻辑看似合理,却导致下游出现故障。
Git diff 在这里帮不上忙。当变更跨越数十个文件时,发现合同违规就像大海捞针。
测试分类系统
我设计了一个测试分类系统,以了解哪些测试在 AI 辅助开发中真正提供价值。
| 等级 | 范围 | 目的 |
|---|---|---|
| L1 | 方法 / 类 | 验证单元正确性 |
| L2 | 命名空间内的跨类 | 验证内部协作 |
| L3 | 命名空间边界 | 检测内部契约变化 |
| L4 | 公共 API 边界 | 保护外部契约 |
每个测试类都使用其等级进行标记,例如:
[Trait("Level", "L3")] // namespace boundary test
多次 AI 重构循环后的观察
| 等级 | 存活情况 | 原因 |
|---|---|---|
| L1 | ❌ 灭绝 | AI 编写了正确的代码;没有检测价值 |
| L2 | ❌ 灭绝 | AI 保持内部一致性 |
| L3 | ✅ 存活 | 检测到命名空间边界违规 |
| L4 | ✅ 存活 | 保护外部 API 合约 |
- L1 和 L2 测试消失 – 并非刻意删除,而是变得毫无意义。AI 重写了内部实现,导致测试要么:
- 轻易通过(测试已经是正确的代码)
- 需要不断更新(追随实现的变化)
- 测试已不存在的代码
- L3 和 L4 测试存活 – 它们捕获了真实问题:超出预期范围的接口更改、API 边界的行为转变,以及 AI 在未理解外部依赖的情况下“改进”的合约。
重新思考 AI 开发的单元测试
传统的单元测试问:“这段代码正确吗?”
AI 时代的测试应问:“合同边界是否被违反?”
这不是大爆炸式测试或经典的集成测试。这是边界测试——明确标记并保护架构中的接缝,防止更改悄无声息地传播。
实践指南
- 明确标记测试层级——该属性具有双重作用:测试过滤和 AI 感知。
- 关注命名空间边界——内部类可以自由更改;它们的聚合接口应保持稳定。
- 绝对保护公共 API——这些是你的外部合同。
- 放弃 L1/L2——不要坚持维护没有信号的测试。
- 利用标签——当 AI 遇到 L3/L4 测试时,标签本身传达:“此边界重要。此处的更改需要验证。”
细粒度测试仍然重要的场景
- 异常处理和边缘情况——AI 擅长正常路径,但可能遗漏细微的错误条件。
- 明确测试异常场景、边界条件和失败模式的测试仍然提供信号——这并不是因为 AI 编写了错误代码,而是因为这些路径在常规的 AI 驱动开发中可能未被触及。
结论
在 AI 辅助开发中,单元测试的角色从正确性验证转变为 变更检测。能够存活下来的测试是那些在有意义的边界——命名空间和公共 API 级别——保护合约的测试。
停止测试 AI 是否写出了正确的代码。开始测试 AI 是否保留了你的合约。
有关实现示例,请参阅 Ksql.Linq 中的测试结构——这是一个 AI 辅助的开源项目,这些模式正是在实践中演化而来的。