开源社区实现 Meta 的 TestGen-LLM 工具,支持用 LLM 自动生成单元测试,提升测试生成效率。
今年 2 月,Meta 的研究人员发表了一篇题为《在 Meta 使用大型语言模型自动改进单元测试》(Automated Unit Test Improvement using Large Language Models at Meta)的论文,介绍了一个名为 TestGen-LLM 的工具。这套全自动提升测试覆盖率的方法,号称能够“保证相较于现有代码库有所改进”,在软件工程领域引发了广泛关注。
Meta 并未公开 TestGen-LLM 的代码,因此我们决定在开源的 icwcount-github-cover-agent 中实现它,并于今天正式发布!
本文将介绍我们是如何实现 TestGen-LLM 的,分享过程中得到的一些发现,并梳理在真实代码库中实际使用 TestGen-LLM 时遇到的挑战。
使用 Generative AI 自动生成测试并不是什么新鲜事。大多数擅长生成代码的 LLM,例如 ChatGPT、Gemini 和 Code Llama,都有能力生成测试。开发者使用 LLM 生成测试时,最常见的问题是:大多数生成出来的测试根本无法运行,还有许多测试并不能带来实际价值,例如,它们测试的功能早已被其他测试覆盖。
为了解决这一问题——具体来说,是针对回归单元测试——TestGen-LLM 的作者提出了以下标准:
测试能否正确编译并运行?
测试能否提高代码覆盖率?
如果连这两个最基本的问题都无法得到肯定的答案,那么可以说,我们根本没有必要接受或分析 LLM 提供的生成测试。确认这些测试能够正确运行,并且能够提升被测组件的覆盖率之后,我们才可以在人工评审中进一步考察:
测试写得怎么样?
它实际上带来了多少价值?毕竟我们都知道,Code Coverage 有时只能作为一个代理指标,甚至可能只是虚荣指标。
它是否满足我们可能提出的其他要求?
TestGen-LLM 和 Qodo Cover 都可以完全以 headless 模式运行——好吧,某种程度上是这样;这一点我们稍后再讨论。
首先,TestGen-LLM 会生成一批测试;然后过滤掉无法构建或运行的测试,并移除所有无法通过的测试;最后,再丢弃那些不能提高代码覆盖率的测试。在高度受控的场景中,生成测试与通过全部步骤的测试之比为 4:1;而在真实场景中,Meta 的作者报告这一比例为 20:1。
自动化流程结束后,Meta 会让人工评审者接受或拒绝这些测试。作者报告的平均接受比例为 2:1;在他们公布的最佳案例中,接受率达到了 73%。
需要特别指出的是,论文中描述的 TestGen-LLM 工具每次运行只会生成一个测试,并将其添加到现有测试套件中;这个测试套件此前由专业开发者编写。此外,它并不一定能为任意给定的测试套件生成测试。
论文中写道:“在三次 test-a-thon 活动中,共有 196 个测试类得到了成功改进,而 TestGen-LLM 工具总共应用于 1,979 个测试类。因此,在应用 TestGen-LLM 的测试类中,约有 10% 能够得到自动改进。”
Qodo Cover v0.1 的实现流程如下:
接收以下用户输入:
被测代码的 Source File
需要增强的现有 Test Suite
用于构建和运行测试套件的命令
代码覆盖率目标和最大迭代次数
额外的上下文与 prompt 选项
以相同风格生成更多测试
使用你的运行时环境验证这些测试:
它们能否完成构建并通过测试?
通过检查代码覆盖率是否提高等指标,确保这些测试能够带来价值
更新现有 Test Suite 和 Coverage Report
重复执行,直到代码满足以下条件之一:达到代码覆盖率阈值,或者达到最大迭代次数
在尝试将 TestGen-LLM 论文中的方法付诸实践时,我们遇到了一些出人意料的挑战。
论文中的示例提到使用 Kotlin 编写测试——这门语言不依赖具有语法意义的空白字符。另一方面,对于 Python 这样的语言,制表符和空格不仅重要,更是解析引擎正常工作的必要条件。GPT 3.5 等能力较弱的模型,即便得到了明确的 prompt,也无法始终返回缩进正确的代码。例如,在 Python 编写的测试类中,每个测试函数都必须缩进,这时就会出现问题。我们必须在整个开发生命周期中处理这一点,导致预处理库周边的复杂度进一步增加。为了让 Qodo Cover 在这类场景中足够稳健,我们仍有许多可以改进的地方。
在试验过程中,我们遇到了各种特殊的测试要求和例外情况。看到这些情况之后,我们决定允许用户在 Qodo Cover 流程中提供额外输入或指令,并将其作为 prompt 的一部分传递给 LLM。–additional-instructions 选项允许开发者提供与自身项目相关的任何额外信息,从而根据需要定制 Qodo Cover。例如,可以使用这些指令引导 Qodo Cover 创建更加丰富的测试,并覆盖有意义的边界情况。
随着 Retrieval-Augmented Generation(RAG)在 AI 应用中日益普及,我们也得出了与这一整体趋势一致的结论:为单元测试生成提供更多上下文,可以生成质量更高的测试,并提高测试通过率。我们为希望手动添加额外库或文本设计文档作为 LLM 上下文的用户提供了 –included-files 选项,以增强测试生成过程。
需要多轮迭代才能处理的复杂代码,也给 LLM 带来了另一个挑战。随着失败的测试或不能带来额外价值的测试不断生成,我们开始注意到一种模式:之前未被接受的同一批测试,会在后续迭代中被反复提出。为了解决这个问题,我们在 prompt 中增加了一个 “Failed Tests” 部分,把这类反馈传递给 LLM,确保它生成不同的测试,并且不再重复那些被我们判定为不可用的测试,例如已经损坏或无法提高覆盖率的测试。
整个过程中出现的另一个挑战是:在扩展现有测试套件时,无法添加库的 import。开发者在生成测试时有时会视野狭窄,只采用某一种测试框架方案。除了各种不同的 mocking 框架之外,其他库也有助于实现测试覆盖率目标。由于 TestGen-LLM 论文和 Qodo Cover 的目标都是扩展现有测试套件,因此完全重构整个测试类并不在其范围之内。在我看来,这是测试扩展相较于测试生成的一项局限,也是我们计划在后续迭代中解决的问题。
需要明确区分的一点是:在 TestGen-LLM 的方案中,每生成一个测试,都需要开发者进行人工评审,之后才会提出下一个测试。而在 Qodo Cover 中,我们会持续生成、验证并提出尽可能多的测试,直到达到覆盖率要求,或者达到最大迭代次数;整个过程无须人工干预。我们利用 AI 在后台运行,以一种不打扰开发者的方式自动生成测试,让开发者可以在流程结束后一次性评审整个测试套件。
包括我在内,许多人都对 TestGen-LLM 论文和工具感到兴奋,但本文也分享了它的局限。我认为,我们目前仍处于 AI 助手的时代,尚未进入由 AI 队友运行全自动工作流的时代。
与此同时,我们计划在 Qodo Cover 中持续开发并分享经过精心设计的流程。这些流程可以帮助开发者自动生成候选测试,并用以往一小部分的时间提高代码覆盖率。
我们计划继续开发与测试生成领域相关的前沿方法,并将它们集成到开源的 Qodo Cover 仓库中。我们鼓励所有对使用 generative AI 进行测试感兴趣的人参与协作,共同扩展 Cover Agent 的能力;我们也希望启发研究人员使用这一开源工具,探索新的测试生成技术。
我们已经在 GitHub 上的开源 Qodo Cover 仓库中添加了一份开发路线图。无论是按照这份路线图,还是根据你自己的想法,我们都非常期待你为这个仓库作出贡献!
我们对 Qodo Cover 的愿景是:未来,它可以在每次 pull request 前后自动运行,并自动提出回归测试增强方案;这些方案已经过验证,能够正常工作并提高代码覆盖率。我们设想 Qodo Cover 可以自动扫描你的代码库,并为你创建包含测试套件的 PR。
让我们利用 AI,更高效地处理那些我们不喜欢做的任务!
我们仍在寻找适用于这类工具的优秀 benchmark。你知道这样的 benchmark 吗?我们认为,这对后续开发和研究至关重要。
你也可以了解我们的 qodo Flow 工作,其中包括:(a)关于 “Flow Engineering” 的延伸阅读;(b)一个竞技编程 benchmark 示例;以及(c)一个名为 CodeContests、设计完善的数据集。
在 Qodo,我们相信,为忙碌的专业开发团队提供代码完整性解决方案,可以解决一系列关键问题和痛点。毕竟,谁会希望自己的软件充斥着服务中断、严重 bug 或糟糕的可维护性呢?