一个AI模型给Python模块写了69个测试,全部通过。而它们合起来,对我故意植入的11个Bug,一个都没抓到。

第二套方案,不指向整个模块,而是直接指向那些具体的Bug,用了17次尝试,抓到了10个

打开网易新闻 查看精彩图片

这个对比,就是整个项目的全部意义。

我搭了什么

一套变异测试工具。它用很小的方式改动源代码——翻转一个比较、改掉一个常量、删掉一个raise——然后跑现有的测试套件,记录哪些改动没被测试发现。一个没人抓到的改动,就是你的测试检测不出来的故障。

覆盖率告诉你某一行跑过了。这套工具告诉你的是:如果那一行是错的,会不会有任何东西失败。这是两个完全不同的数字。一个只有一个happy-path测试的玩具模块,行覆盖率显示47%,21个变异里只抓到2个。

然后我把一个agent指向测试漏掉的变异。对每一个变异,它写一个测试,这个测试只有在干净代码上通过、且在那个特定变异上失败时才会被保留。真值就是一个子进程的退出码。没有任何模型来评判结果。

我发现了什么

三种方案,同一个模型,同样的token上限,十二个广泛使用的Python库,包括cachetools、toolz、tenacity和boltons。生成了455个变异,其中53个存活下来,并且落在测试实际执行到的行上。

我手工审计了它漏掉的九个。七个可证明无法杀死,其中六个是TYPE_CHECKING块里的类型注解,运行时根本不执行。所以,在46个本来能抓到的里面,抓到了44个。

有三个发现,我认为比这个数字更重要。

大多数没被检测到的故障,是没被执行到的代码,不是断言太弱。133个存活变异里,只有53个落在测试执行到的行上。我怀疑是我的测试命令范围划得太窄,于是把每一个都扩大了6到40倍。数量从54变成53。降了。在成熟的、人写的代码里,测试主要不是执行了代码却不检查它,而是主要根本没执行它。

那道门一次都没拒绝过坏测试。74次尝试里,每一个被拒绝的草稿都是一个有效的、能通过的测试,只是没能检测出故障。没有一个是有问题的。这道门在实践中只有一项工作,而且不是我为它设计的那项工作。

它写的测试只能抓到给它看的那个故障,几乎抓不到别的。44个保留测试里,跨函数迁移为零。44个里有36个只抓到一个变异。这是个让人不舒服的结果,它应该和那个44放在一起,而不是压在下面。

它真正重要的地方

搭这个agent花了一个周末。剩下的时间都花在发现我的测量工具一直在骗我。

工具本身有11个Bug。可编辑安装让变异变得不可见。并行执行污染了一个目标。一个分类器跑在了错误的单元上。字节码缓存返回了过期结果。一个结果桶从来没被填满过,因为它匹配的字符串,我这个版本的pytest根本不输出。而我把这个空桶当成一个发现发布了出去。

它们有两个共同点。

每一个都让结果看起来更好,或者让一个"没有"看起来像证据。这是选择,不是阴谋。调试是被意外触发的,而一个令人满意的结果并不意外。所以那个用来剔除测量Bug的过滤器,被不均匀地施加了——对你讨厌的结果很硬,对你喜欢的结果很软。那些讨好你的,活到了发布。

没有一个是通过读代码发现的。每一个都是靠跑一个我事先预测了结果的检查、然后得到了错误答案才抓到的。

然后我发布了,三个读者又找到了三个。全都是指向我的检查,从没指向我的数字。没有人质疑过任何一个结果。每一次修正都落在了工具上。

我会告诉你什么

如果你为自己的工作搭评估,工具才是值得发布的那部分。一个结果是一个人们可以接受也可以不接受的主张。一个工具是别人可以攻击的东西,而那些攻击才告诉你它到底行不行。

我从三个评论线