ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

单元测试写不完?大模型给单测,工具给「测试工程」

单元测试写不完?大模型给单测,工具给「测试工程」

引言

功能代码写完,我总要磨蹭很久才开始写测试。单个方法要覆盖正常路径、异常分支、边界值;依赖要 Mock,断言要能拦住回归;写完还得能编译、能跑绿。

后来试过让大模型生成,测试确实能编译,但我不敢直接信——Mock 对吗?断言够强吗?边界值呢?追问到第三轮,我才意识到它根本没「读」我的代码,只是在对着方法签名猜。

这里有一条值得挑明的分界线:大模型能写「一个测试」,但它写不出「一个项目的测试体系」。

前者是一个代码问题——这个方法的条件分支有哪些;后者是一个工程问题——全项目用什么测试框架、Mock 方案怎么统一、改一处代码哪些用例会坏、该补哪些。本文用一段真实的注册逻辑,对比「对着签名猜」和「先解析代码结构再生成」两种做法的差别。


让大模型写测试的三个坑

  1. 框架版本是第一个坑。JUnit 4 的@RunWith(MockitoJUnitRunner.class)和 JUnit 5 的@ExtendWith(MockitoExtension.class)结构完全不同,断言 API 也换了一轮;要不要引入 Spring Boot Test、@SpringBootTest该用在集成场景还是单元场景,差之毫厘谬以千里。大模型默认给最常见写法,未必匹配项目实际用的那套。

  2. Mock 依赖靠猜。真实代码的依赖可能是构造器注入、静态方法、内部new出来的对象。大模型不知道依赖是怎么进来的:@InjectMocks只 Mock 了一半依赖,另一半是 null,跑起来 NPE;或者 Mock 了不该 Mock 的实现,测试根本没隔离,悄悄走了真实逻辑还断言通过。

  3. 分支覆盖欠账。异常分支和边界值全靠你提示。「帮我写个测试」它交一个正常路径;「加个参数为空的用例」才补一个;「密码太短呢」「用户名重复呢」再补一轮。对代码结构的分析,欠在第一步就没做——它看到的是签名,不是控制流。


单元测试生成器:先解析结构,再生成用例

飞算JavaAI 的 AI 工具箱里有专门的单元测试生成器,思路和「对着签名猜」正好相反:先自动解析业务代码的结构——控制流、分支条件、依赖注入、异常路径——再按结构生成用例。它支持 JUnit 4/5、Mockito 等主流测试框架,并且适配项目实际用的那套,而不是默认给一套通用写法。

配套能力在智能问答(JavaChat)里也能触达:基于项目上下文的问答支持「生成单元测试」,在行内选中方法即可生成,和生成器共享同一套「看得见代码结构」的前提。

步骤动作产出
解析读取方法的控制流、分支条件、依赖注入方式代码结构模型
生成按结构展开正常路径、异常、边界、业务冲突用例结构化用例集
适配匹配项目实际用的 JUnit 版本与 Mock 方案可直接编译运行的测试

拿一段注册逻辑为例:

publicUserDTOregister(RegisterRequestrequest){if(request==null||!StringUtils.hasText(request.getUsername())){thrownewIllegalArgumentException("用户名不能为空");}if(request.getPassword().length()<6){thrownewIllegalArgumentException("密码长度不能少于6位");}if(userRepository.existsByUsername(request.getUsername())){thrownewBusinessException("用户名已存在");}Useruser=newUser();user.setUsername(request.getUsername());user.setPassword(passwordEncoder.encode(request.getPassword()));userRepository.save(user);returnUserMapper.toDTO(user);}

结构一旦被解析出来,用例维度就是确定的:if分支对应异常和边界,构造器里的两个依赖对应两个 Mock,save的副作用对应 verify 断言。它不是靠提示词补出来的,是从代码里读出来的。


实测:同一个 register 方法,写两遍

让通用大模型写的典型演进过程(示意)

  1. 第 1 轮:「为UserService.register生成单元测试」→ 返回一个正常路径用例:@Mock UserRepository,@InjectMocks UserService。编译能过,跑起来 NPE——passwordEncoder没有被 Mock 也没有被注入,执行到encode()时炸了。
  2. 第 2 轮:把堆栈贴回去 → 它补@Mock PasswordEncoder。
  3. 第 3 轮:你问「request为空的用例呢」→ 补一个 NullPointerException 用例——但业务里抛的是IllegalArgumentException,断言类型错了,能跑绿却测不出问题。
  4. 第 4~5 轮:你问「密码太短呢」「用户名重复呢」→ 逐个补,每补一个你都要自己指出下一个缺口。
  5. 第 6 轮:你检查断言,发现「用户名已存在」的用例没校验save是否被调用——改天有人把这行删了,测试照样绿。
  6. 第 7~8 轮:你发现它默认写的是 JUnit 4 的@RunWith,项目其实在用 JUnit 5,再让它统一改……

通常要 8~12 轮,用例维度才勉强凑齐,而每一轮结论都要自己验证、自己指路。它给的是「第一轮交作业」的测试:

// 对着签名猜:只知道要 Mock 仓库,不知道密码编码器从哪来@ExtendWith(MockitoExtension.class)classUserServiceTest{@MockprivateUserRepositoryuserRepository;@InjectMocksprivateUserServiceuserService;@Testvoidregister_withValidRequest_shouldReturnDto(){UserDTOdto=userService.register(validRequest());assertNotNull(dto);// 执行到 passwordEncoder.encode() 时抛 NPE}}

用飞算JavaAI 单元测试生成器

解析代码结构后,一次产出正常路径、异常、边界值、业务冲突四个维度的用例,Mock 依赖按解析出的注入方式补齐,断言按业务语义落到位:

// 基于代码结构生成:Mock 完整、分支全覆盖、断言可拦截回归@ExtendWith(MockitoExtension.class)classUserServiceTest{@MockprivateUserRepositoryuserRepository;@MockprivatePasswordEncoderpasswordEncoder;@InjectMocksprivateUserServiceuserService;@Testvoidregister_withValidRequest_shouldSaveAndReturnDto(){when(passwordEncoder.encode("abc12345")).thenReturn("encoded");UserDTOdto=userService.register(validRequest());verify(userRepository).save(any(User.class));assertEquals("encoded",dto.getPassword());// 断言加密结果,而不是原密码}@Testvoidregister_withBlankUsername_shouldThrow(){RegisterRequestbad=newRegisterRequest();assertThrows(IllegalArgumentException.class,()->userService.register(bad));}@Testvoidregister_withPasswordLength5_shouldThrow(){// 边界值:5 位不通过,另有 6 位通过的用例,分界点两侧各覆盖一次}@Testvoidregister_withDuplicateUsername_shouldThrowAndNotSave(){when(userRepository.existsByUsername("tom")).thenReturn(true);assertThrows(BusinessException.class,()->userService.register(request("tom")));verify(userRepository,never()).save(any());// 冲突不落库,删了 save 测试会红}}

在多个内部项目实测中(参考口径),对存量业务方法生成测试用例集,飞算JavaAI 生成的用例直接通过编译并跑绿的比例约在 85%~90%;而同等需求下通用 AI 的首次可用率往往只有 30%~40%——剩下的要么 Mock 落在 null 上,要么断言太弱,要么框架和项目对不上,全部要人肉返工。


为什么值得重视:单测的终点是「测试工程」

单元测试在一个工程里是成体系的东西。体系意味着三件事:

  • 框架统一(全项目同一套测试框架和 Mock 方案,新成员不需要猜)
  • 风格一致(命名、断言、用例结构可预期)
  • 覆盖可维护(改一处代码,能预判哪些测试会坏、该补哪些)

单拎出来,大模型每一条都能「做到」——适配 JUnit 5、模仿命名风格、提醒补用例。但它每次从零猜你的上下文,没法把「你的测试体系」当成一个持续存在的输入。

单元测试生成器长在工程里:知道项目用 JUnit 几、Mockito 怎么配、当前代码结构什么样,每次生成都基于对真实代码的解析,体系感是持续存在的,而不是每轮对话重新搭一次。


结语

写测试真正花时间的是理解「一段代码在什么条件下走到哪条分支」——这是代码问题,大模型能回答;把对一个项目的测试做成体系、持续对齐框架,是工程问题,需要长在工程里的工具。

大模型给单测,工具给测试工程,我日常两者都在用,只是用途不一样。

选型思考:评估 AI 的测试能力时,不妨多问一句——它是在补一个用例,还是在维护一套测试体系?


相关阅读

  • 飞算JavaAI 官方文档(新手指引 / AI 工具箱):https://www.feisuanyz.com/docs/languages/help.html
  • 下一篇:让 AI 守规矩:用自然语言定义团队规范,而不是靠人肉 review。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进