利用AI Agent技能实现不稳定测试的自动化调试
如果你已经在互联网上冲浪一段时间了,一定听说过 AI Agent Skills。它们能够教会你的 AI Agent 执行各种任务。你甚至可能已经使用或编写过其中的几个了。
如果你还不熟悉它们,其核心概念很简单:与其每次都为特定任务编写提示词(Prompt),不如将这些指令定义一次,以便日后重复使用。一个 Skill 就是知识库文章的 AI 等价物:它是一份存放在可发现位置的纯文本文件,描述了具体的操作步骤、一套约定或特定领域的知识。
你在市面上看到的大多数 Skill 通常用于简单的任务,例如强制执行代码风格或提交信息规范。但它们的功能远不止于此。在本文中,我们将结合 AI Skills、传统的开发工具和一点创造性思维,来解决一个出了名的难题:让 AI 确定性地找出不稳定测试(Flaky tests)的根本原因。
问题所在
引用 TeamCity CI/CD 指南 中的定义:
不稳定测试(Flaky tests)是指在代码或测试本身没有任何更改的情况下,有时通过有时失败的测试。
不稳定性破坏了测试的意义:当测试失败时,你无法判断到底是哪里出了问题。你既不能完全依赖测试结果,也不能完全忽略它们。这浪费了人力和基础设施资源。
如果底层的 Bug 本身不够难处理也就罢了,不稳定测试通常还有一种特性:在数千次运行中可能只会失败一次,这使得它们极难复现和调试。
示例项目
对于示例项目,我们采用这篇文章 《你的程序不是单线程的》 中的 webshop 演示项目。这是一个 Spring Boot 项目,其中一个服务存在 TOCTOU(检查时机与使用时机不一致) 问题:它检查一个条件然后对其采取行动,但另一个线程可能在此期间更改了状态。在这个特定案例中,它有时会导致发票编号重复,并使相应的测试变得不稳定。
以下是有问题的测试代码:
该测试并发创建两个订单,并检查生成的发票编号是否为 INV-00001 和 INV-00002。由于 InvoiceService 中的 Bug,它可能会随机通过或失败。
注意: 如果你使用的是 IntelliJ IDEA,可以通过测试运行器中的“运行直到失败”(Run until failure)选项来测试一个测试是否真的不稳定。让怀疑有问题的测试运行一段时间,看看它最终是否会失败。
如果我们对底层 Bug 一无所知,手里只有这个测试,有没有什么工具可以帮助我们找到根本原因?或者我们可以自己制作一个吗?此外,我们能否将构建和使用该工具的工作都委托给 AI?
直觉
让我们为这类问题建立一些直觉。
为了产生两种不同的结果,程序的执行过程必须遵循不同的代码路径。这种差异可能非常微小,可能只是多调用了一个方法,或者进入了不同的 if 分支。但差异必须存在,否则结果将是一致的。因此,如果我们能记录下一次成功运行和一次失败运行的代码路径,然后进行比较,差异之处至少应该能为我们指明方向。理想情况下,通过跟踪调用树,我们可以找到执行路径的分叉点。这一行代码正是不稳定性的根源所在。
这个推理有道理吗?让我们来验证一下。
构建工具
我们可以用什么工具来记录代码路径?虽然不是专门为追踪而设计的,但测试覆盖率工具可以提供我们需要的信息。
市面上有几种 Java 覆盖率工具可供选择,例如 JaCoCo 和 IntelliJ IDEA 的覆盖率工具。我们将使用 IntelliJ IDEA 的工具,因为它包含一个非常实用的命中计数(hit counting)功能。由于不稳定性可能不仅源于“执行了什么”,还源于“执行了多少次”,因此我们可能需要这种额外的粒度。
从命令行运行覆盖率分析
IntelliJ IDEA 的覆盖率工具拥有熟悉的界面,但我们需要一种以编程方式启动它的方法。幸运的是,通过 Maven Surefire 将覆盖率 Agent 附加到 JVM 上,也可以从命令行收集覆盖率数据:
-Didea.coverage.calculate.hits=true 标志告诉 Agent 记录每行的调用次数,而不仅仅是一个布尔值的“命中/未命中”掩码。测试结束后,结果会写入一个二进制 .ic 文件。
目前为止一切顺利,但我们需要一种人类(和 AI)可读格式的报告。
添加文本输出
幸运的是,IntelliJ 覆盖率 Agent 是开源的。让我们克隆该项目,并让 AI 添加一个将二进制报告转换为纯文本的文本报告器。
Agent 创建了一个名为 TextCoverageStatistics 的新类。在我们构建项目并针对我们的 .ic 文件运行报告器后,我们得到了如下内容:
报告的第一部分提供了高层概述:整个项目中覆盖了多少行、分支和方法。下方是按类细分的指标。
紧接着是每个类的逐行命中计数:
对于覆盖率 Agent 插桩的每一行,我们都能看到它被执行了多少次,以及是否进入了分支。现在我们有了执行代码行数以及执行次数的文本表示。这就是我们进行差异比对所需的原材料。
比对报告差异
理论上,获得的报告包含了必要信息,一位极其坚定的开发者可以仔细研究它们并找到 Bug。但我们不是为了做这种琐事而来的,对吧?
让我们升级这个工具,使其能够获取多个报告变体并呈现差异。最可控的方法是每次处理一个“块”,但我认为在这里将整个过程(包括自动化)委托给 AI 是安全的。
生成的脚本会循环运行测试,直到满足以下两个条件:
- 我们至少得到一次成功运行和一次失败运行。
- 指定次数的运行已经完成。
这两个条件都很重要,因为测试失败可能非常罕见,指定的次数可能不足以捕获失败。同时,成功和失败的运行中可能存在更细微的差异,所以我们可能也想捕捉到这些。
收集报告后,脚本会汇总两次运行之间存在差异的代码行。结果如下:
所有差异都具有相同的模式:区别不在于执行了哪些行,而在于执行了多少次。正如我们所料,IntelliJ IDEA 覆盖率 Agent 的命中计数功能派上了大用场!
这些变化的行指向了 InvoiceService 中的一个延迟初始化块及其在 InvoiceNumberGenerator 和 Invoice 中的下游影响。命中计数的差异意味着初始化有时会运行不止一次,而这本不应该发生。这就是不稳定性的根源所在。
覆盖率差异分析直接指出了我们在上一篇文章中讨论的同一个 TOCTOU 竞态条件。但我们目前方法的创新之处在于,它不完全依赖于人类专家经验,并且易于被 AI 使用。
转化为 Skill
我觉得在几分钟内通过 AI 辅助修改开源工具来解决手头任务本身就非常惊人。但让我们放眼全局。
到目前为止我们做了什么:我们从一个直觉开始——不稳定测试走的是不同的代码路径,覆盖率分析可以揭示它们的分歧点。然后我们将这种直觉转化为了一个具体的、可重复的程序。这值得写成一篇知识库文章,或者一个 AI Agent Skill 吗?当然!
在同一个 Agent 会话中,我们让 Agent 执行以下操作:
- 确保所有脚本都是自包含且可运行的。
- 将整个过程记录在
SKILL.md文件中,一步步详细说明,以便另一个 Agent 在没有先验知识的情况下也能照做。
Agent 将所有内容打包并编写了一份指南,描述了何时应用该 Skill、需要什么工具以及遵循哪些步骤。审查期间唯一的后续工作是使该 Skill 符合 规范。Agent 编写的原始 Skill 缺少 Frontmatter 元数据,虽然 Agent 很擅长处理忽略细节的 Skill,但元数据对于可发现性至关重要。没有它,Skill 可能根本不会被其他 Agent 发现。
测试 Skill
为了验证该 Skill 是否真的有效,让我们开启一个新的 Agent 会话。没有任何预热,没有任何提示。相反,让我们故意以非常通用的方式提问,比如“查找并修复 InvoiceServiceTest 中不稳定性的原因”。
Agent 将 SKILL.md 中的 Skill 描述与问题描述进行匹配,发现了说明文档并执行了它们:它运行了覆盖率脚本,读取了差异报告,并确定了竞态条件。它不再是盲目猜测,而是遵循既定的步骤,每次都能得出相同的结论。这大概就是生成式 AI 所能达到的最确定性的程度了!
总结
我们对覆盖率 Agent 所做的更改已经发布在 1.0.774 版本中。该 Skill 可在 此处 获取。
在本文中,我们从关于不稳定测试的直觉出发,围绕开源覆盖率 Agent 构建了自定义工具,用它找到了竞态条件,并将整个过程封装成了一个可重用的 AI Skill。你可以在自己的项目中使用此 Skill 来查找不稳定测试,但我希望这篇文章传达了更宏大的理念。
AI Skills 允许你教导 Agent 解决几乎任何问题,只要你能将文本接口堆叠在一起。许多困难的编程问题都可以分解为更简单的问题,并使用熟悉的工具来解决。在 AI 的协调下,我们甚至可以让这个过程变得充满乐趣。正如 AI 出现之前一样,好奇心是唯一的真正先决条件。
你是否也受到启发,想要解决工作中棘手的问题?欢迎在评论区分享你编写的或你觉得最有用的 Skills!
祝调试愉快!