我让 AI 写单元测试,结果它连 Mock 都帮我配好了
前几天我们组搞代码质量整改,要求所有核心模块单测覆盖率 80% 以上。我看着自己写的那堆 Service 和 Controller,内心直接崩溃:这得补多少测试类?
我一个写业务代码的,写单测是最痛苦的。倒不是不会,是太枯燥了。同样的 Mock 逻辑写几十遍,断言写来写去就那几种,写着写着就开始怀疑人生。
然后我干了件所有懒人都会干的事:把这活儿扔给了 AI。
一开始出来的测试代码,好看是真好看,跑起来绿也是真绿,但仔细一看——很多断言根本没测到点上,Mock 对象配了一大堆但没验证调用。用行话说就是:覆盖率虚高,有效性堪忧。
后来我琢磨出一套“人机协作”的流程,让 AI 把 80% 的体力活干了,我自己把关剩下 20% 的关键逻辑。今天就把这套方法完整地给大伙儿捋一遍。
一、先搞明白:AI 写单测,优势和短板各在哪
AI 写单测有几个天然优势:
快:一个 Service 的测试类,手写半小时,AI 三十秒;
全:它能覆盖到很多你没想到的边界情况,比如空值、异常、超大入参;
规范:格式统一,命名规律,比人手写的一会儿 test1 一会儿 test_2 强多了。
但它也有几个致命的短板,不知道的话会被坑死:
断言太浅:它喜欢断言“方法执行完没抛异常就成功”,而不是验证正确的返回值或状态;
Mock 乱配:Mock 了一个方法调用,但被测代码里根本没走那个分支;
不理解业务:它不知道哪个字段的边界值对你的业务是致命的,比如金额为负。
所以我的态度很明确:AI 写骨架 + 你写灵魂。 让 AI 干重复的体力活,你来把关关键的验证逻辑。
二、先搭个能跑的环境:让 AI 知道你在测什么
AI 不会凭空知道你的项目结构。你给它一个类,它能猜个大概,但想要高质量的单测,你得把上下文给足。
我的标准操作:把被测类、相关依赖、甚至数据模型,一次性喂给 AI。
比如我要测一个 OrderService.createOrder() 方法,我不会只贴这个方法。我会把这几样东西一起发给 AI:
OrderService 类的完整代码;
Order 实体和 OrderItem 实体;
依赖的 OrderMapper、InventoryService 接口;
项目的 pom.xml 里关于 JUnit、Mockito 的依赖(让 AI 知道版本);
一句清晰的 Prompt。
这样 AI 知道了所有依赖关系,生成的 Mock 就不会张冠李戴。
三、实战:用 AI 生成一个 Service 的单元测试
拿最常见的“订单创建”场景来举例。假设已经有如下代码:
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
public Order createOrder(Long userId, List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new BizException("订单项不能为空");
}
for (OrderItem item : items) {
boolean stock = inventoryService.checkStock(item.getProductId(), item.getQuantity());
if (!stock) {
throw new BizException("库存不足");
}
}
Order order = new Order();
order.setUserId(userId);
order.setTotalPrice(items.stream().mapToDouble(OrderItem::getPrice).sum());
order.setStatus("CREATED");
orderMapper.insert(order);
return order;
}
}
我喂给 AI 的 Prompt 是这么写的:
你是一个 Java 后端测试工程师。
使用 JUnit 5 + Mockito。
为上面的 OrderService.createOrder 方法编写完整的单元测试,要求:
覆盖正常流程(至少一个成功创建订单的场景);
覆盖异常流程:items 为空、库存不足;
验证 orderMapper.insert 被调用了一次;
验证返回的 Order 对象字段正确;
使用 @ExtendWith(MockitoExtension.class),对依赖使用 @Mock;
测试方法命名用 should_xxx_when_xxx 格式;
直接输出完整测试类代码,不要解释。
AI 三十秒输出的代码长这样(我稍作精简):
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderMapper orderMapper;
@Mock
private InventoryService inventoryService;
@InjectMocks
private OrderService orderService;
@Test
void should_create_order_successfully_when_items_valid() {
List<OrderItem> items = Arrays.asList(
new OrderItem("prod1", 2, 100.0),
new OrderItem("prod2", 1, 200.0)
);
when(inventoryService.checkStock("prod1", 2)).thenReturn(true);
when(inventoryService.checkStock("prod2", 1)).thenReturn(true);
Order result = orderService.createOrder(1L, items);
assertNotNull(result);
assertEquals(1L, result.getUserId());
assertEquals(400.0, result.getTotalPrice(), 0.01);
assertEquals("CREATED", result.getStatus());
verify(orderMapper, times(1)).insert(any(Order.class));
}
@Test
void should_throw_exception_when_items_empty() {
assertThrows(BizException.class, () -> orderService.createOrder(1L, Collections.emptyList()));
verify(orderMapper, never()).insert(any());
}
@Test
void should_throw_exception_when_stock_insufficient() {
List<OrderItem> items = Arrays.asList(new OrderItem("prod1", 10, 100.0));
when(inventoryService.checkStock("prod1", 10)).thenReturn(false);
assertThrows(BizException.class, () -> orderService.createOrder(1L, items));
verify(orderMapper, never()).insert(any());
}
}
说实话,看到这个输出的时候我自己也有点不淡定。Mock 配置、断言、verify 验证、异常测试,甚至 never() 验证都写全了。如果我自己手写,至少要二十分钟。现在我只花了写 Prompt 的两分钟,加上检查的一分钟。
四、AI 生成单测的常见坑,我替你踩过了
上面那个例子跑得很顺,但不是每次都这么顺利。以下是我反复踩过的几个大坑。
坑一:Mock 了但没验证
AI 经常 Mock 了一堆方法,但在测试里只测了返回值对不对,没去 verify Mock 方法到底有没有被调用,以及被调用了多少次。这在数据库写操作、消息发送等场景里是致命的。
解决方法:Prompt 里显式要求“对关键 Mock 对象的方法调用做 verify 验证”。
坑二:用真实对象而不是 Mock
有时候 AI 会直接把依赖的 Service 用 new 实例化出来,导致测试里调了真实的数据库或外部接口。这是因为它没完全理解依赖注入的上下文。
解决方法:明确告诉 AI “所有外部依赖(Mapper、Service、RPC)全部 Mock,被测对象用 @InjectMocks 注入”。
坑三:断言泛泛而谈
比如只写 assertNotNull(result),这叫测试吗?这叫心理安慰。订单创建出来不为空是应该的,关键是你得验价格算对了没、状态设对了没。
解决方法:Prompt 里加上“断言必须验证关键字段值”,甚至可以给出你关心的字段名。
坑四:异常测试没验异常信息
assertThrows 只是抓到异常,但如果被测代码里可能抛出同一种异常但原因不同,不验 message 就是一笔糊涂账。
解决方法:要求 AI 在异常测试里加上 assertThrows 后对 getMessage() 的验证。
五、进阶:让 AI 自动帮你做边界值分析
除了常规流程,AI 还能干一件特别有意思的事:根据你的代码,推测边界条件并生成用例。
我常用的 Prompt:
分析以下方法的输入参数,列出所有可能的边界值和异常场景,并生成对应的测试用例。
边界包括但不限于:null 值、空集合、零值、负数、超大值、超长字符串。
AI 对一个普通的分页查询方法,能给我列出七八种边界测试:页码为 0、页码为负、每页条数为 0、每页条数超大(比如 10000)、排序字段不存在……这些边界测试虽然看起来啰嗦,但往往是线上 Bug 的温床。
人工做这些,十个人九个漏。AI 做这些,又快又全。
六、Controller 层测试:连 HTTP 请求都给我模拟好了
很多人只给 Service 写单测,Controller 就不管了。其实 Controller 的测试同样重要,而且 AI 写起来更爽——因为结构更固定。
我用的是 MockMvc,让 AI 帮我生成:
使用 JUnit 5 + MockMvc 为以上 Controller 编写测试。要求覆盖:正常请求(200)、参数校验失败(400)、业务异常(返回特定错误码)。
AI 输出的测试类里,perform()、andExpect()、Mock 配置全都写好。你只要改改请求路径和 JSON 字符串就行。
一个省事的小技巧:别手写 JSON 入参,让 AI 根据你的 DTO 自动构造。Prompt 里加一句“请求体 JSON 根据 XxxDTO 结构构造示例数据”,它就能给你生成合法的 JSON。
七、我的日常单测流程,直接抄
补依赖:把被测类、依赖接口、实体类、pom.xml 一起贴给 AI;
写 Prompt:用上面那个模板,说清技术栈、覆盖场景、断言要求;
生成代码:AI 出第一版;
我审查:检查 Mock 调用是否正确、断言是否有效、异常场景是否覆盖;
补充边界:让 AI 再根据代码分析补充边界用例;
跑测试:跑一遍,有失败的地方分析是代码逻辑问题还是测试写错了;
补 Mock 调用:跑覆盖率工具,如果发现某个分支没覆盖到,再让 AI 针对未覆盖分支补用例。
这个流程下来,原来一天的工作量,现在一小时差不多了。
八、最后说两句
AI 写单测,真正解决的其实不是“不会写”的问题,而是“懒得写”的问题。
单元测试这东西,每个程序员都知道重要,但真到自己写的时候,总觉得“代码这么简单,不可能出 Bug”。然后线上出了问题,追悔莫及。AI 的出现,把写单测的心理门槛和体力门槛都打下来了——你只需要动动嘴,AI 就把骨架搭好,你稍微改改就能用。
别把 AI 生成的测试当成品,把它当成一份草稿。你审一审,补一补,就是一份质量不错的单元测试。 人与 AI 配合,一加一真的可以大于二。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)