Mutmut:一个Python变异测试系统
跳到“这有多难?”如果你不关心背景。
什么是突变测试?
变异测试是一种合理确定您的代码实际测试代码的完整行为的方法。不仅像覆盖率报告那样触及所有行,而是实际测试所有行为和所有奇怪的边缘情况。它通过尽可能巧妙地一次更改一个地方的代码并运行测试套件来做到这一点。如果测试套件 succeeds 它算作失败,因为它可能会更改代码,并且您的测试很幸福地没有意识到有任何问题。
突变的例子是将“<”更改为“<u003d”。如果您没有在测试中检查确切的边界条件,您可能拥有 100% 的代码覆盖率,但您将无法通过突变测试。
背景
我想尝试对我用 Python 构建的库进行突变测试,所以我查看了可用的库。基于pytest-testmon所做的事情,我对从根本上加速突变测试的方法有了一些想法,并在上面添加了一些我自己的想法。
谷歌搜索向我展示了两种选择:
-
Mutpy:作为论文开发的一个简单系统。不再维护。
-
宇宙射线:积极发展。
这两个都只是 Python 3,这有点令人难过,因为我仍然在 Python 2 上工作至少一年以上。但我可以忍受,因为库是 Python 2 和 3。
Cosmic Ray 似乎更有希望,所以我尝试安装它,但在努力安装依赖项之后,我决定如果安装就这么难,它可能不实用。我稍微查看了代码,看看我是否可以只使用突变部分作为库,但在我看来它就像一个巨大的单体系统,所以我放弃了。
接下来我看了看mutpy。这段代码从根本上更小更简单,但在努力以某种方式重构它以使其更简单之后,我心想:
有多难?
事实证明,没那么糟糕!大多数构建块已经可用。
我决定我绝对想要 Cosmic Ray 和 mutpy 都缺乏的功能:能够在源文件上应用突变,而不是搞砸整个文件。 Cosmic Ray 和 mutpy 使用 AST 库中内置的 Python,但它具有不代表 AST 中的格式的不幸特性,因此无法将 AST 转储出来并获取原始文件。所以如果我不能使用 Python 自己的 AST,那该怎么办?输入baron,这是一个独立开发的 Python->AST 库,专门创建能够在不更改源文件的情况下往返。不幸的是,Baron 还不支持所有 Python 3 语法,但看起来人们正在研究它。
[编辑:自从这篇文章以来,我已经用 Parso 替换了 Baron,现在我完全支持 Python 3!]
我的作战计划是这样的:
- 制作一个 mutate 函数,它接收源代码并可以改变所有相关的东西(这样你就可以获得可用突变的计数)或索引指定的特定突变。
2.制作一个导入钩子,以便您要变异的文件在从磁盘的途中在内存中变异。这将启用并行化。
-
Pytest 插件,它设置导入钩子并使您能够指定您想要的突变。
-
制作一个小命令行程序,运行突变并检查测试的输出。它还应该能够在磁盘上应用特定的突变,所以当你找到一个有趣的突变时,你可以很容易地看到这个突变是什么。
第 1 点 相当简单:基本上我需要确保所有 AST 节点类型要么没有发生变异(因为它没有意义),要么以我能想到的最讨厌的方式发生变异。在这一步中,我浏览了许多大型开源项目(例如 django 和 numpy)的代码。在此步骤中,我在 baron 中发现了一些解析错误,但没有任何影响我想要运行突变测试的代码。我刚刚报告了错误并继续前进。
第 2 点很讨厌。事实证明,python 中的导入钩子系统非常糟糕。从文件系统加载的默认行为不在钩子列表中,因为它在某处的 C 代码中,因此您不能将导入器基于它。如果设计没问题,那就没问题了,但不幸的是导入钩子系统是这样工作的:Python 一次要求一个导入钩子来导入模块。这听起来简单,简单通常很好,但导入实际上包含以下步骤:
1.找到源文件
2.读取源文件
3.将源文件从文本转换为可运行模块
所有进口商都必须完成所有步骤。因此 zip 文件导入器必须执行与默认相同的步骤,并且它不能只调用默认加载器,因为它不作为 python 代码存在。这也意味着如果我想在所有进口商的第 2 步和第 3 步之间进行拦截,我必须_重新实现所有进口商_。
这显然很糟糕(对于具有自己的自定义导入器的系统,甚至可能是不可能的),但更糟糕的是,完全正确地实现导入钩子是很多不平凡的代码,这是一个要正确的血腥野兽。据说这在带有 importlib 的 python 3 中要好一些,所以我发现它的反向移植到 python 2但它被破坏了。我设法解决了崩溃问题,但最后我的导入钩子也没有使用它。我也在 reddit上寻求帮助,但无济于事。
经过几个小时的战斗,我放弃了(现在),只是选择了基于磁盘的突变。这不是很好,因为它不能并行运行,但至少它可以工作并且超级简单。它对于您使用的测试运行器也非常灵活,因为您不需要为 pytest、nose、unittest 等一一制作的任何插件。放弃这意味着第 3 点 变得毫无意义,所以这很好。
基本上我应该先做这个东西,因为它非常好:P
第 4 点 非常简单。最难的是找出如何在控制台上很好地输出持续的进度更新:P
那么我现在站在哪里?
我们在工作中对tri.declarative和tri.struct运行了 mutmut,它发现了一些我们没有像我们想象的那样彻底测试的东西,即使我们的测试覆盖率为 100%。对于 tri.declarative,它还发现了一段死代码和在错误消息中正确创建复数的错误。它明显改进了我们的测试套件,即使它没有发现任何错误。
您现在可能只能运行 mutmut。这是一段非常简单的代码,要让它与您的测试运行程序一起工作,它只需要它的退出代码为零表示成功,其他任何值表示失败。它显然很慢,因为它根本没有并行化,并且它必须运行您为每个突变指定的整个测试套件(而且有很多!)。
下一步是什么?
我还有一些工作要做。使用 pytest-testmon 仍然在我的列表中,解决导入钩子系统以启用并行化也是如此。仅这些东西就应该能够让我的测试速度提高几个数量级。目标是保持我现在拥有的超级简单的系统,以便始终有一个易于调试和调整的系统,您可以将其用于奇怪的场景或调试。
我对 pytest-testmon 也有一些想法,比如能够在开发人员之间共享一个中央数据库,这也可以用于 mutmut,所以如果有人已经尝试为特定版本的文件运行特定突变,你不会必须。
Hacker Noon是黑客开始他们下午的方式。我们是@AMI系列的一员。我们现在接受提交并很高兴讨论广告和赞助机会。
要了解更多信息,请阅读我们的关于页面、在 Facebook上给我们点赞/留言,或者简单地说,推文/DM @HackerNoon。
如果您喜欢这个故事,我们建议您阅读我们的最新科技故事和热门科技故事。直到下一次,不要把世界的现实视为理所当然!
更多推荐

所有评论(0)