前几天我们组搞代码质量整改,要求所有核心模块单测覆盖率 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 配合,一加一真的可以大于二。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐