AI 测试用例生成——能生成真正有价值的测试吗
AI 测试用例生成——能生成真正有价值的测试吗
适读人群:有单元测试经验的 Java/Python 开发者 | 阅读时长:约 12 分钟 | 核心价值:学会区分 AI 生成的好测试和垃圾测试,建立判断框架
上个月我在帮一个朋友做代码 review,他兴奋地说:"老张你看,我用 AI 把整个 Service 层的单元测试全生成了,覆盖率直接从 20% 干到 85%!"
我看了一眼他的测试代码,沉默了几秒。
不是不好,是绝大多数都是垃圾。测试覆盖率数字很好看,但那些测试根本抓不到真正的 bug。更危险的是,他会产生一种"项目已经有测试保护"的错误安全感。
这两年我在实际项目里大量使用 AI 生成测试,踩了很多坑,也总结出了一套判断框架。今天把这个框架写出来,供你参考。
AI 生成测试的典型场景
我们先来看看 AI 到底能生成什么。我拿一个真实的订单服务来举例:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
public OrderResult createOrder(OrderRequest request) {
// 参数校验
if (request == null || request.getUserId() == null) {
throw new IllegalArgumentException("订单请求不能为空");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new IllegalArgumentException("订单商品不能为空");
}
// 计算总价
BigDecimal totalAmount = calculateTotalAmount(request.getItems());
if (totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
// 检查库存
for (OrderItem item : request.getItems()) {
boolean available = inventoryService.checkStock(item.getProductId(), item.getQuantity());
if (!available) {
throw new InsufficientStockException("商品库存不足: " + item.getProductId());
}
}
// 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setItems(request.getItems());
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
Order savedOrder = orderRepository.save(order);
// 锁定库存
for (OrderItem item : request.getItems()) {
inventoryService.lockStock(item.getProductId(), item.getQuantity());
}
return OrderResult.success(savedOrder.getId());
}
private BigDecimal calculateTotalAmount(List<OrderItem> items) {
return items.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}我把这段代码喂给 AI,让它生成单元测试。AI 生成了大概 15 个测试方法。我来分类评价。
第一类:真正有价值的测试
AI 生成了这个测试,我觉得是好的:
@Test
void createOrder_shouldThrowException_whenRequestIsNull() {
assertThrows(IllegalArgumentException.class, () -> {
orderService.createOrder(null);
});
}
@Test
void createOrder_shouldThrowException_whenItemsIsEmpty() {
OrderRequest request = new OrderRequest();
request.setUserId(1L);
request.setItems(Collections.emptyList());
assertThrows(IllegalArgumentException.class, () -> {
orderService.createOrder(request);
});
}
@Test
void createOrder_shouldThrowInsufficientStockException_whenStockNotAvailable() {
OrderRequest request = buildValidOrderRequest();
when(inventoryService.checkStock(anyLong(), anyInt())).thenReturn(false);
assertThrows(InsufficientStockException.class, () -> {
orderService.createOrder(request);
});
}这三个测试是有价值的,因为它们测试了异常路径和边界条件。在真实项目里,这些 edge case 是最容易出 bug 的地方,也是人工手写时最容易偷懒跳过的地方。
AI 还生成了一个我没想到的 case:
@Test
void createOrder_shouldThrowException_whenTotalAmountIsZero() {
OrderRequest request = new OrderRequest();
request.setUserId(1L);
OrderItem item = new OrderItem();
item.setProductId(1L);
item.setQuantity(1);
item.setPrice(BigDecimal.ZERO); // 价格为0的商品
request.setItems(Arrays.asList(item));
assertThrows(IllegalArgumentException.class, () -> {
orderService.createOrder(request);
});
}这个测试我自己很可能会漏掉——价格为 0 的商品这种场景。AI 通过阅读 calculateTotalAmount 里的逻辑,自动推断出了这个 edge case。这就是 AI 的真实价值:它能从代码逻辑里系统性地推导出各种入参组合,比人工更全面。
第二类:看起来对、其实没用的测试
这是 AI 生成的"主路径"测试:
@Test
void createOrder_shouldSucceed_whenValidRequest() {
OrderRequest request = buildValidOrderRequest();
Order savedOrder = new Order();
savedOrder.setId(1L);
when(inventoryService.checkStock(anyLong(), anyInt())).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
OrderResult result = orderService.createOrder(request);
assertTrue(result.isSuccess());
assertEquals(1L, result.getOrderId());
}这个测试表面上没什么问题,但它本质上只是在测"代码能跑通",没有测任何业务逻辑。如果我把 order.setTotalAmount(BigDecimal.ZERO) 写成 bug,这个测试不会报错。
这种测试只测了 happy path,而且 mock 了太多依赖,导致它实际上只在验证调用链是否正确,不在验证业务逻辑。
更糟糕的版本是这个:
@Test
void createOrder_shouldCallSaveOnce() {
OrderRequest request = buildValidOrderRequest();
when(inventoryService.checkStock(anyLong(), anyInt())).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenReturn(new Order());
orderService.createOrder(request);
verify(orderRepository, times(1)).save(any(Order.class));
}这个测试直接在测实现细节,而不是测行为。它在验证"save 被调用了一次"。但如果我重构代码,把单个 save 拆成两步(先 insert 再 update),这个测试会直接挂掉——即使业务逻辑完全正确。
这是 AI 生成测试最大的问题:它倾向于生成基于实现的测试,而不是基于行为的测试。
第三类:明显的垃圾测试
AI 还生成了这种东西:
@Test
void calculateTotalAmount_shouldReturnCorrectAmount() {
// 通过反射调用私有方法
Method method = OrderService.class.getDeclaredMethod("calculateTotalAmount", List.class);
method.setAccessible(true);
List<OrderItem> items = new ArrayList<>();
OrderItem item = new OrderItem();
item.setPrice(new BigDecimal("10.00"));
item.setQuantity(2);
items.add(item);
BigDecimal result = (BigDecimal) method.invoke(orderService, items);
assertEquals(new BigDecimal("20.00"), result);
}这个测试在测私有方法。私有方法是实现细节,不应该直接测试。一旦你重命名这个方法或改变实现方式,测试就挂了。测试私有方法是典型的"测了实现而不是行为"的错误。
判断框架:四个问题
两年下来,我总结了一个判断 AI 生成测试质量的框架——四个问题,挨个问:
问题 1:这个测试在验证什么业务规则?
如果答不上来,或者答案是"它在验证 X 方法被调用了",这个测试大概率没价值。好的测试应该能对应到一条具体的业务需求。
问题 2:如果我改变实现方式但保持行为不变,这个测试会挂吗?
会挂的测试是基于实现的测试。比如把 for 循环改成 stream,把一个方法拆成两个,这些重构不应该导致测试失败。
问题 3:这个测试能抓住哪种 bug?
好的测试应该能抓住具体的 bug 类型。"边界值为空时应该抛异常"能抓住空指针问题。"库存不足时应该抛 InsufficientStockException"能抓住错误处理问题。如果一个测试很难说清楚它能抓住什么 bug,它可能只是在刷覆盖率。
问题 4:这个 case 在真实生产环境中会出现吗?
测试的核心价值是防止生产 bug。如果一个 case 在生产环境中不可能出现(比如某个字段永远不可能为 null,因为有数据库约束),这个测试的价值就很低。
AI 生成测试的合理工作流
基于这些经验,我现在的工作流是这样的:
让 AI 做什么:
- 生成边界条件和异常路径测试——AI 在这方面比人工更系统
- 生成参数组合的 data provider,配合参数化测试使用
- 为已有的测试补充缺失的 assertion
自己做什么:
- 删掉所有基于实现细节的测试(verify 调用次数、测私有方法)
- 把 happy path 测试改成真正验证业务规则的形式
- 补充 AI 没想到的业务特有场景(比如你们系统里的特殊用户类型、特殊商品类型)
实际操作示例:
我在 AI 生成测试后,会用这个 prompt 让它自己做一遍 review:
这是你刚才生成的测试。请按照以下标准评估每一个测试方法:
1. 它测试的是行为还是实现?
2. 它对应的是哪条业务规则?
3. 如果我重构实现但保持行为不变,它会不会挂?
把你认为质量低的测试标记出来,说明原因。AI 通常能正确识别出 60-70% 的低质量测试。剩下的 30% 需要你自己判断。
一个反直觉的结论
用了两年 AI 生成测试,我得出了一个反直觉的结论:
AI 在测试生成上的最大价值不是减少你写测试的时间,而是帮你发现你没想到的 edge case。
如果你用 AI 是为了"快点把覆盖率刷上去",结果会是一堆没价值的测试,还会给你虚假的安全感。
但如果你用 AI 是"帮我检查一遍,有没有我遗漏的边界条件",它能真实地帮你写出更好的测试套件。
我朋友那个项目,最后我们一起把 AI 生成的 15 个测试砍到了 7 个,但这 7 个测试都是真正有价值的。覆盖率从 85% 降回了 65%,但保护质量提高了一倍不止。
覆盖率是个虚荣指标。测试抓 bug 的能力才是实际价值。
