cover_image

和手写单元测试用例说再见~

杨杰 三七互娱技术团队
2024年07月29日 10:01
01

背景

图片
图片

单元测试用例是高质量代码的必要条件,可以帮助验证代码正确性,同时保证重构和修改代码的安全性。在国内技术部后端核心代码的单元测试覆盖率要达到70%以上,否则在CI流水线就会被拒绝发布上线。这个标准保证了业务的稳定和代码的质量,但也给开发小伙伴带来了巨大的时间成本,特别是在PHP转Go的这个重构的阶段,开发业务代码和写单元测试用例的耗时比例达到了7:3(针对复杂代码可能到6:4),因此对于第一位重构某个业务的小伙伴来说是痛苦的,本人也是其中之一(╥╯^╰╥)。

本文的目标就是解放研发生产力,通过AI自动生成“可运行”的单元测试用例,同时通过验证单元测试用例的正确性和覆盖率,以此作为反馈要求AI生成更多的单元测试用例覆盖代码。

02

现状

图片
图片

目前直接使用 AI 生成单元测试用例有以下几个问题:

  • 生成的单元测试用例不能直接运行,需要人为调整才能运行。

  • 没法评估覆盖率,可能生成了一个单元测试并且能跑,但是不提升覆盖率(和已有的冲突)。

  • 直接在测试文件(*_test.go)中生成能运行的单元测试用例。(这点是最难的)

针对以上三个问题,我们将采用目前比较流行的 `反思工作流` 方式,让 AI 不断生成可以提升覆盖率的单元测试用例。

吴恩达最近开源的翻译 agent 采用的就是 `反思工作流` 方式,据说生成的翻译内容可以媲美商业翻译软件。

03

设计思路

图片
图片

整体的设计思路:让 AI 感知单元测试用例的覆盖率,从而生成更多单元测试用例,通过“生成 → 验证 → 反馈 → 再生成 → 满足要求”的反馈工作流实现整体流程。

图片

我们以一个go文件a.go,目标是70%覆盖率的需求为例,讲解下具体流程:

  • 将 a.go 和 a_test.go 通过 go test  命令生成当前的单元测试覆盖率报告 coverge.out。

  • 通过 report convert 将 coverage.out 转换成 AI 可读的覆盖率报告。例如哪些行没有被覆盖,哪些行已经被覆盖了,生成构建提示语的覆盖率情况。

  • 将上一步的覆盖率情况以及单测文件作为 prompt builder 的输入,生成 AI 大模型需要的提示语。

  • 请求 AI 大模型获取输出交给单元测试生成器 unit test generator,解析 AI 大模型的输出,将其变成对应的单元测试代码。

  • 将新增的单元测试代码追加到对应的 a_test.go 的文件中,然后评估覆盖率是否满足 70%,如果不满足则调到第2步,满足则退出。

04

设计细节

图片
图片

AI 生成单元测试用例的整体思路并不复杂,但是细节是魔鬼,很多地方需要处理,比如:如何让 AI 生成对应的内容只是代码不夹带一些解释说明,如何让 AI 感知覆盖率的情况(哪些行覆盖了的,哪些行没有覆盖)等。

覆盖率报告解析器

图片
图片

go test 在运行单元测试用例时(使用 go test -coverprofile=coverage.out),可以生成覆盖率报告 coverage.out ,具体格式如下:

gapimysdk.37.com/internal/pkg/account.go:32.46,35.2 2 1gapimysdk.37.com/internal/pkg/account.go:37.46,40.2 2 1gapimysdk.37.com/internal/pkg/account.go:42.60,43.34 1 1gapimysdk.37.com/internal/pkg/account.go:43.34,44.35 1 1gapimysdk.37.com/internal/pkg/account.go:44.35,46.4 1 1gapimysdk.37.com/internal/pkg/account.go:49.2,49.18 1 1gapimysdk.37.com/internal/pkg/account.go:52.63,53.46 1 1gapimysdk.37.com/internal/pkg/account.go:53.46,55.3 1 1gapimysdk.37.com/internal/pkg/account.go:57.2,58.16 2 1gapimysdk.37.com/internal/pkg/account.go:58.16,61.3 2 1gapimysdk.37.com/internal/pkg/account.go:63.2,63.56 1 1

格式定义:文件路径:起始行号.起始列号,结束行号.结束列号 语句数 执行次数

  • 文件路径: 表示被测试的Go源文件的路径。

  • 行号和列号:

    • 起始行号.起始列号: 表示代码块的开始位置

    • 结束行号.结束列号: 表示代码块的结束位置

  • 语句数: 表示这个代码块包含的语句数量。

  • 执行次数: 表示这个代码块在测试过程中被执行的次数

这种格式的可读性很差,也不好让 AI 感知具体的代码覆盖率,因此我们构建一个 report convert 模块,最终得到的就是一个自定义的报告格式,生成自定义报告的目的是让 AI 能够更加清晰的理解目前单元测试用例的覆盖率,最终生成一个提供给 AI 的简报,格式如下:

====== 当前覆盖率描述 ======
覆盖行:[10, 20, 30, .....]
未覆盖行:[11, 22, 35,.....]
当前代码覆盖率:35.2%

以上简报的格式可以比较好地让 AI 感知到当前的文件覆盖率是多少,哪些行没有覆盖。这里可能会有个疑问,单纯告诉 AI 这些行号有用么?有用,我们在提示语构建中会说明这个用处。

提示语构建

图片
图片

要想实现 AI 逐步生成可运行并且不断提升覆盖率的单元测试用例,就不能使用例如:“请帮我生成单元测试用例,函数是 xxx" 的方式,这种方式生成的单元测试用例甚至可能跑不起来。

因此需要精心构建这个提示语,我们采用 角色式提示 + 结构化提示 + 示例式提示 的方式,具体模式如下:

图片

其中,第1点钟的行动准则是为了要求 AI 如何逐步生成单元测试用例,并且自己反思确认生成是否有问题。第2点中源码文件是带有行号的,这就和第4点中的覆盖率简报中的行数对应上了,效果还是挺好的。通过以上方式可以从 AI 的获得较好的响应结果(以上的描述是示例,真实的比较长就不贴在这了),并且提取到对应的结构中,生成可以运行的单元测试用例。

单元测试用例修正

图片
图片

AI 返回的内容虽然可以尽可能规范它的格式,但是难免有的时候返回夹杂一些私活导致整个返回不能直接使用,比如内容返回中带有"``",带有 "{" 或者 "}",还有换行格式的问题,这些都是要统统处理掉,这样才能保证 AI 生成的代码是可以运行的。

图片

生成的单元测试用例是需要经过一个验证的阶段,验证的时候会有三个结果:运行报错、运行成功-增加覆盖率,运行成功-覆盖率未提升,针对这三个情况需要应对处理:

  • 运行报错:收集报错信息反馈给大模型,让它修正问题并生成新的单元测试。

  • 运行成功 - 增加覆盖率:判断是否达到目标的覆盖率。

  • 运行成功 - 覆盖率未提升:用新的提示语告诉 AI 目前生成的单元测试用例没有用,换个思路取思考如何覆盖。

05

总结

图片
图片

1、本文分享了基于 AI 生成单元测试用例的完整流程,目前已经跑通整个流程,并且可以在无人为参与纠正的过程中逐步通过自反馈逼近目标的单元测试用例。

2、后续可以通过将该功能接入 CI 流程中,自动生成单元测试用例,解放研发生产力。



END


三七互娱技术团队

扫码关注 了解更多

图片

继续滑动看下一个
三七互娱技术团队
向上滑动看下一个