用 AI 辅助生成单元测试:从测试场景梳理到断言校验的实践记录
在很多研发团队里,单元测试经常处在一个比较尴尬的位置。
大家都知道它重要,但实际开发中经常会遇到这些情况:
- 业务需求赶进度,单测被放到最后;
- 只测了正常流程,异常分支没有覆盖;
- 老代码逻辑复杂,不知道该怎么补测试;
- Mock 写起来麻烦,开发者不愿意写;
- 代码改动后,没有测试兜底,回归风险很高。
ChatGPT、Claude、Gemini、DeepSeek 这类 AI 助手,正好可以在这个环节发挥作用。它们不能替代开发者理解业务,但可以帮助我们快速梳理测试场景、生成测试代码初稿、补充边界用例,从而降低写单测的启动成本。
本文以一个订单取消接口为例,分享一套 AI 辅助单元测试生成的实践流程。
一、为什么单元测试适合用 AI 辅助
单元测试通常有比较固定的结构:
- 准备输入参数;
- Mock 依赖方法;
- 调用被测方法;
- 校验返回结果;
- 校验异常;
- 校验依赖方法是否被调用。
这类模式化工作,很适合交给 AI 生成初稿。
比如我们可以让 AI 帮忙:
- 根据代码列出测试场景;
- 生成 JUnit 测试代码;
- 补充 Mockito Mock 逻辑;
- 检查断言是否完整;
- 发现遗漏的异常分支;
- 生成参数化测试用例;
- 根据测试失败信息分析原因。
但需要注意,AI 生成的单测并不一定正确。它可能会:
- 假设不存在的方法;
- 写出与项目版本不兼容的语法;
- Mock 错对象;
- 忽略真实业务规则;
- 只断言“不为空”,没有验证核心逻辑;
- 为了让测试通过,反而修改业务含义。
所以正确使用方式应该是:让 AI 生成测试草稿,再由开发者根据业务规则和项目规范校验。
二、示例场景:订单取消服务
假设有一个订单取消方法,代码如下:
java
@Servicepublic class OrderService {
@Autowired private OrderRepository orderRepository;
@Autowired private StockService stockService;
public void cancelOrder(Long orderId, Long userId) { if (orderId == null || userId == null) { throw new BizException("参数不能为空"); }
Order order = orderRepository.findById(orderId); if (order == null) { throw new BizException("订单不存在"); }
if (!Objects.equals(order.getUserId(), userId)) { throw new BizException("无权限取消该订单"); }
if (!"WAIT_PAY".equals(order.getStatus())) { throw new BizException("当前订单状态不允许取消"); }
order.setStatus("CANCELED"); orderRepository.update(order);
stockService.releaseStock(order.getOrderNo()); }}
这个方法逻辑不算复杂,但已经包含多个测试点:
- 参数为空;
- 订单不存在;
- 用户无权限;
- 订单状态不允许取消;
- 正常取消订单;
- 取消后是否更新状态;
- 是否释放库存;
- 异常情况下是否不调用库存服务。
如果手动写单测,容易只覆盖正常流程。但 AI 可以帮助我们先列出完整场景。
三、先让 AI 生成测试场景,而不是直接写代码
很多人使用 AI 写单测时,会直接输入:
text
帮我给这段代码写单元测试。
这样 AI 确实能生成代码,但质量不稳定。更好的方式是先让它分析测试场景。
Prompt 示例:
text
你是一名有经验的 Java 后端测试开发工程师,请根据下面的 Service 代码,先不要写测试代码,只输出需要覆盖的单元测试场景。
要求:1. 按正常流程、异常流程、边界条件分类;2. 每个场景说明输入、Mock 行为、预期结果;3. 标注哪些依赖方法应该被调用,哪些不应该被调用;4. 如果有业务规则不确定,请标注“需要确认”。
待测试代码:【粘贴 OrderService 代码】
这样做的好处是,开发者可以先 Review 测试场景,确认没有跑偏,再让 AI 继续生成测试代码。
AI 可能会输出类似场景:
| 场景 | 输入 | Mock | 预期 |
|---|---|---|---|
| orderId 为空 | orderId=null,userId=1 | 无 | 抛出参数不能为空 |
| userId 为空 | orderId=1,userId=null | 无 | 抛出参数不能为空 |
| 订单不存在 | orderId=1,userId=1 | findById 返回 null | 抛出订单不存在 |
| 用户不匹配 | order.userId=2 | findById 返回订单 | 抛出无权限 |
| 状态非 WAIT_PAY | status=PAID | findById 返回订单 | 抛出状态不允许 |
| 正常取消 | status=WAIT_PAY | findById 返回订单 | 更新状态为 CANCELED,并释放库存 |
这一步非常重要,因为测试代码写得再漂亮,如果测试场景错了,也没有意义。
四、再让 AI 生成 JUnit + Mockito 测试代码
场景确认后,可以继续输入:
text
请根据以上测试场景,使用 JUnit 5 和 Mockito 生成单元测试代码。
要求:1. 使用 @ExtendWith(MockitoExtension.class);2. 使用 @Mock 和 @InjectMocks;3. 异常场景使用 assertThrows;4. 正常场景校验 orderRepository.update 被调用;5. 正常场景校验 stockService.releaseStock 被调用;6. 异常场景校验 update 和 releaseStock 不被调用;7. 不要使用项目中不存在的工具类。
AI 可能生成如下代码:
java
@ExtendWith(MockitoExtension.class)class OrderServiceTest {
@Mock private OrderRepository orderRepository;
@Mock private StockService stockService;
@InjectMocks private OrderService orderService;
@Test void cancelOrder_orderIdIsNull_shouldThrowException() { BizException exception = assertThrows(BizException.class, () -> { orderService.cancelOrder(null, 1L); });
assertEquals("参数不能为空", exception.getMessage()); verify(orderRepository, never()).findById(any()); verify(orderRepository, never()).update(any()); verify(stockService, never()).releaseStock(any()); }
@Test void cancelOrder_orderNotFound_shouldThrowException() { when(orderRepository.findById(1L)).thenReturn(null);
BizException exception = assertThrows(BizException.class, () -> { orderService.cancelOrder(1L, 1L); });
assertEquals("订单不存在", exception.getMessage()); verify(orderRepository).findById(1L); verify(orderRepository, never()).update(any()); verify(stockService, never()).releaseStock(any()); }
@Test void cancelOrder_userNotMatch_shouldThrowException() { Order order = new Order(); order.setId(1L); order.setUserId(2L); order.setStatus("WAIT_PAY");
when(orderRepository.findById(1L)).thenReturn(order);
BizException exception = assertThrows(BizException.class, () -> { orderService.cancelOrder(1L, 1L); });
assertEquals("无权限取消该订单", exception.getMessage()); verify(orderRepository).findById(1L); verify(orderRepository, never()).update(any()); verify(stockService, never()).releaseStock(any()); }
@Test void cancelOrder_statusNotWaitPay_shouldThrowException() { Order order = new Order(); order.setId(1L); order.setUserId(1L); order.setStatus("PAID");
when(orderRepository.findById(1L)).thenReturn(order);
BizException exception = assertThrows(BizException.class, () -> { orderService.cancelOrder(1L, 1L); });
assertEquals("当前订单状态不允许取消", exception.getMessage()); verify(orderRepository).findById(1L); verify(orderRepository, never()).update(any()); verify(stockService, never()).releaseStock(any()); }
@Test void cancelOrder_waitPayOrder_shouldCancelAndReleaseStock() { Order order = new Order(); order.setId(1L); order.setOrderNo("O202501010001"); order.setUserId(1L); order.setStatus("WAIT_PAY");
when(orderRepository.findById(1L)).thenReturn(order);
orderService.cancelOrder(1L, 1L);
assertEquals("CANCELED", order.getStatus()); verify(orderRepository).findById(1L); verify(orderRepository).update(order); verify(stockService).releaseStock("O202501010001"); }}
这类代码虽然还需要根据项目实际包名、异常类、实体类字段进行调整,但已经节省了不少重复劳动。
五、AI 生成单测后,必须重点检查什么
1. 断言是否真的验证了业务结果
有些 AI 生成的测试只会写:
java
assertNotNull(result);
这种断言价值很低。好的单测应该验证关键业务结果,例如:
- 状态是否从
WAIT_PAY变成CANCELED; - 是否调用了库存释放;
- 异常情况下是否没有更新订单;
- 错误消息或错误码是否符合预期。
2. Mock 是否符合真实调用路径
AI 有时会假设方法名,例如:
java
orderRepository.getById(orderId)
但真实代码中是:
java
orderRepository.findById(orderId)
这类问题需要人工修正。
3. 异常分支是否校验副作用
异常测试不只是校验“抛异常”,还要校验不该发生的动作没有发生:
java
verify(orderRepository, never()).update(any());verify(stockService, never()).releaseStock(any());
这可以防止代码在异常前已经产生错误副作用。
4. 测试是否过度依赖实现细节
有些测试会过度校验内部调用顺序,导致代码稍微重构就大量失败。单测应该关注核心行为,不要把每一行内部实现都锁死。
六、让 AI 帮忙补充边界用例
第一轮单测写完后,可以继续让 AI 检查遗漏:
text
请检查以上单元测试是否还有遗漏场景。重点关注:1. 空值;2. 非法状态;3. 权限问题;4. 外部依赖异常;5. 是否需要事务;6. 是否存在并发或重复取消问题。
AI 可能会提示:
order.getUserId()为空时如何处理;order.getStatus()为空时是否应该抛异常;stockService.releaseStock抛异常时订单状态是否已经更新;- 重复取消同一订单是否需要幂等;
- 取消订单是否应该加事务;
- 状态枚举是否应该使用常量或枚举类。
这些问题有些属于单测,有些属于业务设计或代码 Review,需要开发者进一步判断。
例如库存释放失败时,当前代码是先更新订单状态,再释放库存:
java
orderRepository.update(order);stockService.releaseStock(order.getOrderNo());
如果释放库存失败,订单已经变成取消状态,但库存没有释放,就可能出现数据不一致。AI 可能会建议加事务,但如果 stockService 是远程服务,普通数据库事务也解决不了分布式一致性问题。这时就需要人工结合架构判断,而不是简单照抄建议。
七、多模型在单元测试场景中的使用方式
在单元测试生成方面,不同模型可以这样分工:
| 模型 | 适合任务 |
|---|---|
| ChatGPT | 快速生成测试场景和 JUnit 代码 |
| Claude | 处理较长 Service 代码、复杂业务流程 |
| Gemini | 根据需求文档整理测试点 |
| DeepSeek | 中文业务语境下分析异常分支和边界条件 |
如果是关键业务代码,可以采用“两轮 AI”方式:
- 第一个模型生成测试场景;
- 第二个模型检查测试场景是否遗漏;
- 开发者确认后再生成代码;
- 本地运行测试并修正。
如果团队使用 KULAAI 这类多模型聚合平台,也可以把同一段代码分别交给 ChatGPT、Claude、Gemini、DeepSeek 对比输出。不同模型关注点不同,交叉检查有助于发现遗漏。
八、推荐的 AI 单测生成流程
比较稳妥的流程是:
- 粘贴被测方法和相关 DTO、枚举;
- 让 AI 先输出测试场景,不写代码;
- 人工确认测试场景;
- 指定 JUnit、Mockito、Spring Boot Test 等版本;
- 让 AI 生成测试代码;
- 本地运行测试;
- 修复编译错误和 Mock 问题;
- 检查断言是否有效;
- 用覆盖率工具查看遗漏分支;
- 将最终测试纳入 CI。
这个流程看起来比“直接让 AI 写代码”多了几步,但最终质量会稳定很多。
九、注意事项
使用 AI 生成单元测试时,建议注意以下几点:
- 不要输入未脱敏的生产数据;
- 不要让 AI 直接决定业务异常码;
- 不要为了让测试通过而降低断言质量;
- 不要只测正常流程;
- 不要忽略外部依赖异常;
- 不要把 AI 生成代码不运行就提交;
- 不要把单测覆盖率当成唯一目标。
单元测试的核心不是追求覆盖率数字,而是让关键业务逻辑在未来修改时有自动化兜底。
十、总结
AI 辅助单元测试最大的价值,是帮助开发者降低“写测试的启动成本”。
它可以帮我们:
- 快速拆解测试场景;
- 补充异常分支;
- 生成测试代码初稿;
- 提醒副作用校验;
- 输出边界用例清单。
但最终仍然需要开发者确认:
- 测试场景是否符合业务;
- Mock 是否符合真实调用;
- 断言是否有效;
- 异常分支是否完整;
- 测试是否能在本地和 CI 稳定运行。
一句话总结:
AI 可以帮你更快写出单元测试初稿,但测试是否真的可靠,仍然取决于开发者对业务逻辑的理解和验证。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)