上个月,领导丢给我一个“祖传”项目——代码堆了五年,文档约等于零,单元测试覆盖率常年趴在20%不动。每次上线前,全靠几个人手动回归,点得手指抽筋。领导说:“给你两周,把覆盖率提到60%以上,不然下个版本我们不敢发。”
我盯着屏幕上的20%,又看看那几十个屎山一样的模块,陷入了沉思。手工补?一个函数一个函数地写断言?两周怕是连一个核心模块都啃不完。
这时候,我决定赌一把:让AI来给我打工。
一、 开局:直接扔代码给AI,它给我吐了个寂寞
我的第一个想法很简单:把代码贴给 ChatGPT,让它写单元测试。
于是我从一个工具类开始,贴了大概 200 行代码,附上指令:“请为这个类编写完整的 JUnit 单元测试,使用 Mockito。”
AI 反应很快,10秒就给我生成了一个测试类,洋洋洒洒上百行。我满心欢喜地复制到 IDEA 里,一运行——全红。
报错信息乱七八糟:NoSuchMethodError、NullPointerException、Mockito cannot mock this class……我仔细一看,AI 生成的测试里,mock 了一些私有方法,还 mock 了 final 类(那个项目用的 Java 8,Mockito 默认不支持 mock final)。更要命的是,它假设了一堆根本不存在的 when(...).thenReturn(...),而那些依赖的真实行为,AI 根本不知道。
第一个教训:AI 不知道你的项目上下文,它只会“看着代码写测试”,但看不懂你的依赖环境、框架限制、以及那些隐藏在配置文件里的 Bean。
二、 改变策略:我当“翻译”,AI 当“码农”
既然 AI 对全局一窍不通,那我就把全局拆碎了喂给它。我开始尝试 “逐方法 + 带上下文” 的策略。
2.1 先给 AI 讲业务,再给代码
对于每一个核心方法,我不再只贴代码,而是先用自然语言描述:
- 这个方法在什么场景下调用?
- 输入参数有哪些约束?比如 userId 必须是正数,否则抛异常。
- 它会调用哪些外部服务?比如会调用
userService.getUserById(),那个服务可能返回 null 或抛异常。 - 它内部有哪些分支逻辑?if-else 的条件是什么?
比如有一个 calculateDiscount 方法,里面调用了会员等级接口,根据等级返回折扣率。我这样描述:
“这是一个电商订单的折扣计算函数。它接收一个
userId和一个orderAmount。它会先调用membershipClient.getLevel(userId)获取会员等级(可能的返回值:'BRONZE', 'SILVER', 'GOLD')。如果是 GOLD,折扣率为 0.8;SILVER 为 0.9;BRONZE 或 null 为 1.0。然后计算orderAmount * discountRate并返回,结果保留两位小数。如果orderAmount为负数,抛出 IllegalArgumentException。”
然后我把方法代码贴给它,让它生成测试。这次生成的测试,终于能把几个分支覆盖到了,但是——它只写了 happy path。
比如它只会测试 GOLD 返回正常折扣,但不会测试会员接口抛异常的情况,也不会测试 orderAmount 为 0 或者负数的情况。覆盖率数字上去了,但质量堪忧。
2.2 用“缺陷驱动”让 AI 补全边界
这时候我换了个指令:“请针对这个方法,设计包含以下场景的测试用例:1. 正常流程;2. 会员接口超时抛异常;3. 会员接口返回 null;4. orderAmount 为 0;5. orderAmount 为负数;6. orderAmount 为极大值(防止溢出)。”
AI 这才像模像样地生成了覆盖异常和边界的测试。运行一下,居然有几个挂了!仔细一看,是代码里确实没处理 orderAmount 为负数的情况——AI 帮我找到了一个隐藏的 bug。我赶紧修了代码,再跑测试,绿了。
第二个教训:AI 需要你告诉它“要测什么”,你不说的,它默认不测。把测试场景列清楚,它才能生成高质量的用例。
三、 批量化:用脚本让 AI 批量打工
一个方法一个方法地跟 AI 聊,虽然比手写快,但还是太慢。我算了一下,两周时间,就算不吃不喝也聊不完几百个方法。
于是我写了个小脚本(Python),做了个简陋的“AI 测试生成流水线”:
- 扫描项目中的所有 Java 文件,提取出 public 方法(用正则匹配,粗糙但够用)。
- 对每个方法,读取它所在的类全名,以及方法签名。
- 如果这个类还没有对应的测试类(或者覆盖率报表显示它低于某个阈值),就把它加入待生成队列。
- 调用 OpenAI API,传入我精心设计的 Prompt(包含业务描述模板、代码片段、测试框架要求、以及常见的边界场景列表)。
- 将生成的测试代码写入到对应的
*Test.java文件中,并自动格式化。
当然,第一次批量生成的几百个测试,一大半都跑不过。主要问题:
- import 缺失:AI 生成的 mock 对象没有导入对应的类,或者导错了包名。
- 静态方法 mock 失败:有些工具类用的是静态方法,AI 直接 mock,但 PowerMock 没配。
- 依赖未初始化:Spring 环境下的 @Autowired 依赖,AI 直接 new 出来,导致 NPE。
3.1 引入 Mock 框架和父类
针对这些问题,我做了两件事:
- 为每个测试类自动加上
@RunWith(MockitoJUnitRunner.class)和必要的 import。 - 写了一个基础的测试父类
BaseUnitTest,里面初始化了 Mockito,并提供了通用的 mock 工厂方法。 - 在 Prompt 中明确要求:“使用 Mockito 进行 mock,不要 mock 静态方法,除非你确信项目已经集成了 PowerMock。对于 Spring 注入的依赖,统一使用 @Mock 注解。”
经过几轮调整,生成的测试通过率从 20% 提升到了 70% 左右。剩下的 30%,大多是 AI 理解错了业务逻辑,或者代码实在太复杂(比如多线程、异步回调),需要我手动干预。
四、 覆盖率虚高?还得靠变异测试来检验
覆盖率数字蹭蹭涨,很快到了 75%。但我心里没底:这些 AI 生成的测试,真的能测出代码的问题吗?
我在一个核心类上跑了一下 PITest(变异测试) ,看看这些测试能不能杀死变异体。结果惨不忍睹:变异测试覆盖率只有 30% 左右。也就是说,AI 生成的测试大多是“表面覆盖”,只调用了代码,但没有对返回值做充分的断言,所以即使把代码的逻辑改掉,测试也不会失败。
第三个教训:AI 生成的测试经常缺少强有力的断言。它可能只是调用了方法,然后 assertNotNull(result),根本不检查结果是否符合预期。这样的测试对提升代码质量几乎没有帮助。
于是我修改了 Prompt,加入了一段“优秀断言示例”,比如:
对于返回 BigDecimal 的方法,请使用
assertEquals(0, expected.compareTo(actual))而不是assertEquals(expected, actual),因为浮点数精度问题。 对于集合,请验证大小和关键元素,不要只assertNotNull。 对于抛异常的场景,请用assertThrows验证异常类型和消息。
并且,在生成测试后,我会运行变异测试,如果某个方法的变异体没被杀掉,就把变异体信息反馈给 AI,让它重新生成针对性的断言。这样迭代几轮,变异覆盖率也慢慢爬到了 60% 以上。
五、 结果与反思
两周时间,我几乎没怎么写代码,但通过“人指挥 AI”的方式,把项目的单元测试覆盖率从 20% 拉到了 82% (行覆盖)和 67% (分支覆盖)。更重要的是,在这个过程中,AI 帮我发现了 7 个隐藏的 bug,都是之前手工测试没覆盖到的边界情况。
现在每次 CI 都会自动运行这些测试,团队终于敢对老项目做重构了。而我,也从“手工点点点”的测试,变成了“AI 训练师+测试架构师”的混合体。
关于我们
霍格沃兹测试开发学社,隶属于 测吧(北京)科技有限公司,是一个面向软件测试爱好者的技术交流社区。
学社围绕现代软件测试工程体系展开,内容涵盖软件测试入门、自动化测试、性能测试、接口测试、测试开发、全栈测试,以及人工智能测试与 AI 在测试工程中的应用实践。
我们关注测试工程能力的系统化建设,包括 Python 自动化测试、Java 自动化测试、Web 与 App 自动化、持续集成与质量体系建设,同时探索 AI 驱动的测试设计、用例生成、自动化执行与质量分析方法,沉淀可复用、可落地的测试开发工程经验。
在技术社区与工程实践之外,学社还参与测试工程人才培养体系建设,面向高校提供测试实训平台与实践支持,组织开展 “火焰杯” 软件测试相关技术赛事,并探索以能力为导向的人才培养模式,包括高校学员先学习、就业后付款的实践路径。
同时,学社结合真实行业需求,为在职测试工程师与高潜学员提供名企大厂 1v1 私教服务,用于个性化能力提升与工程实践指导。