AI 辅助生成单元测试:从一行提示词到可信的测试用例
写单元测试是最容易被拖延的工作:业务代码写完就想提交,测试总想着“下次补”。最近我在 Java 项目里试着把这部分工作交给 AI,发现它确实能把写测试的时间从“一下午”压缩到“半小时”,但前提是人要把好两道关:说清楚测什么,复核生成的断言。
一、先讲清楚行为,再让 AI 写测试
AI 写测试的质量,取决于你给的信息是否完整。直接丢一段代码说“帮我写测试”,得到的往往是一堆只覆盖 happy path 的用例。比较有效的做法,是在提示词里一次讲清四件事:
- 被测对象和它的职责;
- 正常的输入输出是什么;
- 边界条件有哪些,比如空值、零、负数、上限;
- 异常情况应该抛什么错、返回什么。
以一个订单折扣计算为例。被测类 DiscountCalculator 依赖外部的会员折扣服务 PriceService,规则是:会员折扣率最高按 30% 封顶,金额必须为正数,结果保留两位小数、四舍五入。
把这些规则写进提示词,再指定用 JUnit 5 + Mockito 的风格生成,AI 给出的第一版通常已经像模像样。
二、一个能跑的例子:JUnit 5 + Mockito
下面是被测类的核心逻辑(简化版):
public class DiscountCalculator {
private final PriceService priceService;
public DiscountCalculator(PriceService priceService) {
this.priceService = priceService;
}
public BigDecimal calculate(String userId, BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
BigDecimal rate = priceService.vipDiscountRate(userId);
if (rate == null) {
rate = BigDecimal.ZERO;
}
if (rate.compareTo(new BigDecimal("0.3")) > 0) {
rate = new BigDecimal("0.3");
}
return amount.multiply(BigDecimal.ONE.subtract(rate))
.setScale(2, RoundingMode.HALF_UP);
}
}
AI 生成的测试类(经人工复核调整后):
@ExtendWith(MockitoExtension.class)
class DiscountCalculatorTest {
@Mock
private PriceService priceService;
private DiscountCalculator calculator;
@BeforeEach
void setUp() {
calculator = new DiscountCalculator(priceService);
}
@Test
void vipUserGetsDiscount() {
when(priceService.vipDiscountRate("u1")).thenReturn(new BigDecimal("0.2"));
assertEquals(new BigDecimal("80.00"),
calculator.calculate("u1", new BigDecimal("100")));
}
@Test
void discountIsCappedAt30Percent() {
when(priceService.vipDiscountRate("u2")).thenReturn(new BigDecimal("0.5"));
assertEquals(new BigDecimal("70.00"),
calculator.calculate("u2", new BigDecimal("100")));
verify(priceService).vipDiscountRate("u2");
}
@Test
void nullRateMeansNoDiscount() {
when(priceService.vipDiscountRate("u3")).thenReturn(null);
assertEquals(new BigDecimal("100.00"),
calculator.calculate("u3", new BigDecimal("100")));
}
@Test
void nonPositiveAmountThrows() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculate("u1", new BigDecimal("0")));
assertThrows(IllegalArgumentException.class,
() -> calculator.calculate("u1", null));
}
@Test
void resultIsRoundedHalfUp() {
when(priceService.vipDiscountRate("u1")).thenReturn(new BigDecimal("0.1"));
assertEquals(new BigDecimal("9.00"),
calculator.calculate("u1", new BigDecimal("10.005")));
}
}
这个例子里有几个值得注意的点:正常折扣、封顶、空折扣率、非法入参、四舍五入,五个用例覆盖了主要的分支;外部依赖用 Mockito 的 @Mock 隔离,测试不依赖真实的会员服务;verify 只用来确认关键交互,而不是把每个调用都锁死。
三、AI 生成测试常见的三个坑
第一,只测 happy path。AI 容易顺着代码的“主干”写用例,边界和异常分支经常缺失。补边界清单是人的责任,写提示词时就要列出来。
第二,断言“幻觉”。AI 有时会断言代码实际没有的行为,比如断言了某个不存在的返回值。每一行断言都要能对应到代码或需求,跑测试通过还不够,断言本身也得是真的。
第三,把实现细节焊死在测试里。比如对某个内部方法的调用次数做严格校验,或者断言了临时的日志格式。这类测试会在重构时成片变红,维护成本很高。好的测试应该只关心“行为”,而不是“怎么做”。
四、人工复核的三步检查
我现在的做法是:AI 生成之后,一定要做三件事。第一,跑一遍,确认全部通过,并且是真的在跑——有的测试看起来通过,实际是因为 Mock 配错了,根本没走到被测代码。第二,做一次“变异检查”:故意改一行实现,比如把封顶值从 0.3 改成 0.4,看对应的测试会不会变红;如果不变红,说明测试没有真正覆盖这条分支。第三,删掉没有意义的断言和用例,只保留能描述业务行为的测试。
五、把流程沉淀成习惯
用熟练之后,AI 生成测试可以变成固定流程:先写清楚业务规则和边界,再让 AI 按 JUnit 5 + Mockito 的风格生成骨架,最后人工复核断言和边界。华为云的 MaaS 能力可以为这类代码助手提供模型支持,在 AI DevSpace 里也能直接用对话的方式让 AI 阅读代码、生成并运行测试,省掉了来回复制粘贴的步骤。对团队来说,比工具更重要的是约定:新功能合并之前,测试用例和实现代码一起评审,AI 生成的测试也一样。
结语
AI 写单元测试不是“不用写测试了”,而是把人力从重复的骨架代码里解放出来,放到更值得花时间的地方:想清楚边界、校验断言、守住行为。测试的价值从来不在行数,而在它能不能在你改代码的那一刻站出来说“不对”。这一关,AI 替不了人,但人可以借 AI 把它做得更好。
- 点赞
- 收藏
- 关注作者
评论(0)