测试
这一课你将能做到
- 让 AI 生成覆盖正常、边界、异常三类场景的单元测试
- 识别并修正 AI 生成测试的典型缺陷(测试通过但没验证、mock 过度、边界遗漏)
- 建立「AI 生成测试 + 人工审查」的纪律,确保测试真的有用
为什么要学
AI 是生成单元测试的神器——写测试的痛点(繁琐、重复、覆盖不全)恰好是 AI 的强项。但 AI 生成的测试有个隐蔽的坑:它们经常「看起来都对,实际上什么都没测」。
人话:AI 写的测试就像 AI 写的代码一样,需要审查。一个不验证结果的测试(assertNotNull(result) 就完事),一个 mock 掉所有依赖的测试(测的是 mock 不是代码),一个只测 happy path 的测试(边界全靠运气)——这些测试的存在感比没有还危险,因为它们给你虚假的安全感。
真实项目中,测试是 AI 生成代码的最后一道质量门。没有测试的 AI 代码,就像没有验收报告的施工——看起来盖好了,住进去才知道哪面墙是歪的。
先建立图像
AI 生成测试的质量光谱:
┌─────────────────────────────────────────┐
│ 好测试: │
│ ✓ 验证行为(不只验证返回值不为 null) │
│ ✓ 覆盖边界(空、null、极值、并发) │
│ ✓ 验证异常(抛了正确的异常类型和信息) │
│ ✓ 真实依赖或最小 mock │
├─────────────────────────────────────────┤
│ 坏测试(AI 常犯的): │
│ ✗ assertNotNull 就完事 │
│ ✗ mock 掉所有依赖,测的是 mock │
│ ✗ 只测正常路径,不测边界和异常 │
│ ✗ 测试逻辑复制被测代码的 bug │
└─────────────────────────────────────────┘
跟着做
步骤 1:让 AI 生成三类场景的测试
给一个真实的服务方法,让 AI 生成完整测试:
// 被测方法
public Article publish(Long articleId, Long operatorId) {
Article article = articleMapper.selectById(articleId);
if (article == null) {
throw new BusinessException("文章不存在");
}
if (!article.getAuthorId().equals(operatorId)) {
throw new BusinessException("只能发布自己的文章");
}
if ("PUBLISHED".equals(article.getStatus())) {
throw new BusinessException("文章已发布,不能重复发布");
}
article.setStatus("PUBLISHED");
article.setPublishedAt(LocalDateTime.now());
articleMapper.updateById(article);
return article;
}
提示词:
为上面的 publish 方法生成单元测试,要求:
1. 覆盖正常场景:作者发布自己的草稿文章
2. 覆盖边界场景:文章不存在、非作者操作、已发布状态
3. 覆盖异常场景:数据库操作异常
4. 每个测试验证:返回值、数据库状态变更、异常类型和消息
5. 用 JUnit 5 + Mockito,不要 mock 掉被测方法本身
步骤 2:审查 AI 生成的测试
AI 通常会给出一版「看起来不错」的测试。你要按清单审查:
清单 1:是否验证了行为?
// 差:只验证不抛异常
@Test
void publish_success() {
assertDoesNotThrow(() -> articleService.publish(1L, 1L));
}
// 好:验证状态变更和返回值
@Test
void publish_success() {
Article result = articleService.publish(1L, 1L);
assertEquals("PUBLISHED", result.getStatus());
assertNotNull(result.getPublishedAt());
verify(articleMapper).updateById(argThat(a ->
"PUBLISHED".equals(a.getStatus())
));
}
清单 2:是否覆盖了边界?
// AI 常漏掉的边界:
- articleId 为 null
- operatorId 为 null
- article 的 status 为 null(不是 "DRAFT" 也不是 "PUBLISHED")
- publishedAt 时区问题(如果涉及时区)
清单 3:mock 是否过度?
// 差:mock 掉了所有东西,测试失去了意义
when(articleMapper.selectById(any())).thenReturn(mock(Article.class));
when(articleMapper.updateById(any())).thenReturn(1);
// 这个测试能过,但数据库约束、SQL 错误、字段映射问题都测不到
// 好:用 H2 内存数据库或 Testcontainers,测真实 SQL
@SpringBootTest
@AutoConfigureTestDatabase(replace = Replace.ANY)
class ArticleServiceIntegrationTest {
// 真实 Mapper + 真实数据库(H2),测的是真实行为
}
步骤 3:让 AI 补全遗漏的边界
审查后把遗漏点反馈给 AI:
补充以下场景的测试:
1. articleId 为 null 时抛 IllegalArgumentException
2. article 的 status 为 null 时的处理逻辑
3. 并发场景:两个线程同时发布同一篇文章,应该只有一个成功
步骤 4:建立「AI 测试纪律」
写进团队规则文件:
## AI 生成测试的纪律
- 每个测试必须验证行为,不允许只 assertNotNull
- 边界场景(null、空、极值)必须人工检查是否覆盖
- 数据库操作优先用 H2/Testcontainers 集成测试,mock 只用于外部服务
- 并发场景必须测(如果业务涉及共享状态)
- AI 生成的测试和 AI 生成的代码走相同的 Code Review 流程
讲清楚要点
- AI 生成测试的价值在「覆盖」,风险在「虚假覆盖」:AI 能快速生成 20 个测试方法,但其中 15 个可能什么都没验证。审查的核心是「这个测试失败时,能发现什么 bug」。
- mock 的粒度决定测试的价值:mock 掉所有依赖的单元测试,测的是「mock 的行为」不是「代码的行为」。数据库操作用 H2/Testcontainers 集成测试,mock 只留给外部 HTTP 服务。
- 边界和异常是 AI 的盲区:AI 倾向于生成 happy path 测试。null、空、极值、并发、时区——这些需要人工列出清单,让 AI 补。
- 测试代码也是代码,需要 Review:AI 生成的测试可能复制被测代码的 bug(比如被测代码算错了,测试也断言错误的结果)。测试的断言要基于「业务应该是什么样」,不是「代码现在是什么样」。
- 测试是 AI 生成代码的验收标准:没有测试的 AI 代码,你无法确认它「做了什么」和「应该做什么」是否一致。
你可能会懵的地方
-
AI 生成的测试覆盖率很高,是不是就够了? 覆盖率是必要不充分条件。100% 行覆盖率但全是 assertNotNull 的测试,价值为零。关注「分支覆盖率」和「断言质量」,不只是行覆盖率。
-
AI 能生成集成测试和端到端测试吗? 能,但质量参差不齐。集成测试(@SpringBootTest + H2)AI 生成得不错;端到端测试(Playwright/Selenium)涉及页面交互,AI 生成的选择器和等待逻辑常需要手工修正。
-
测试代码也要原子提交吗? 要。测试和被测代码分开提交,或至少分开 commit。这样 Review 时可以单独看「测试是否覆盖了需求」。
-
AI 生成的测试总是通过,但代码明明有 bug,怎么回事? 典型场景:被测代码有 bug,AI 生成的测试「学习」了这个 bug,断言了错误的结果。对策:测试的断言必须基于需求文档或业务规则,不是基于代码实现。
-
老项目没测试,能让 AI 补吗? 能,但要分层补:先补「核心业务流程」的集成测试(给钱相关的、给用户数据的),再补单元测试。补测试时让 AI 先「读代码总结行为」,再生成测试,而不是直接生成。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
assertNotNull(result) 就完事 | 什么都没验证 | 验证状态变更、返回值、异常消息 |
| mock 掉所有依赖 | 测的是 mock 不是代码 | 数据库用 H2 集成测试,mock 只给外部服务 |
| 只测 happy path | 边界全靠运气 | 人工列出边界清单,让 AI 补 |
| 测试断言复制代码 bug | 测试通过但逻辑错误 | 断言基于业务需求,不是代码实现 |
| 覆盖率 90% 就觉得够了 | 虚假安全感 | 看分支覆盖率和断言质量,不看行覆盖率 |
卡点急救
- AI 生成的测试跑不过:把失败信息贴给 AI,让它分析是「测试写错了」还是「代码有 bug」。两者都有可能,AI 倾向于修测试让测试通过,你要判断哪个是对的。
- 老代码没法测(静态方法、私有方法、硬编码依赖):让 AI 先做「可测试性重构」(提取接口、依赖注入),再生成测试。直接给不可测试的代码生成测试,AI 会用反射等黑魔法,测试很脆弱。
- 并发场景的测试 AI 写不好:并发测试本身就很难写(线程调度、时序不确定)。让 AI 生成「基于 CountDownLatch/CyclicBarrier 的确定性并发测试」,或直接用现成框架(如 jcstress)。
面试常问
-
AI 生成测试的最大风险是什么?怎么控制? 高分回答:最大风险是「虚假覆盖」——测试通过但没验证行为。控制方法:纪律上要求每个测试必须验证状态变更或异常;流程上 AI 生成的测试走和代码相同的 Review;技术上用变异测试(mutation testing)工具(如 PIT)验证测试真的能发现 bug。
-
如何为遗留系统补测试? 分层补:先补核心业务流程的集成测试(保障不回归),再补单元测试(提升覆盖率)。补测试前先做可测试性重构(依赖注入、提取接口)。AI 可以加速「读代码总结行为」和「生成测试骨架」两个环节。
-
TDD 在 AI 时代还有意义吗? 有,而且更重要。TDD 的核心价值是「先想清楚应该什么样,再写代码」——这恰好是 AI 协作的核心能力(描述需求+验收结果)。AI 时代可以「AI 辅助 TDD」:你写测试(或让 AI 生成测试骨架),AI 实现代码,你验收测试是否通过。
自测三问
- AI 生成测试的三个典型缺陷是什么?
- 为什么「mock 掉所有依赖」的测试价值很低?
- 为遗留系统补测试的分层策略是什么?
小结一句话
AI 是生成测试的神器,但「测试通过」不等于「质量过关」——审查断言质量、覆盖边界场景、用集成测试替代过度 mock,测试才真正是你的安全网。
延伸
- JUnit 5 官方文档
- Testcontainers 官方文档
- PIT 变异测试
- 下一课:安全——AI 生成代码的安全审查清单。