Python 突变测试简介

通过突变测试将隐藏的错误转化为可见的修复。
123 位读者喜欢这个。
Searching for code

Opensource.com

你为所有内容都编写了测试;也许你的项目仓库中甚至有一个徽章,表明 100% 的测试覆盖率。但是这些测试对你有什么帮助?你如何知道?

单元测试的成本对于开发人员来说是显而易见的。测试必须编写。偶尔,它们无法按预期工作:会出现误报或不稳定的测试,在没有任何代码更改的情况下,成功和失败交替出现。你可以通过单元测试发现的小错误很有价值,但通常它们会在开发人员的机器上悄悄发生,并在提交到版本控制之前修复。但真正令人震惊的错误大多是不可见的。而最糟糕的是,遗漏的警报是完全不可见的:在你将代码交给用户之前,你不会看到你未能捕获的错误——有时甚至在那之后也看不到。

有一种类型的测试可以使不可见变为可见:突变测试

突变测试以算法方式修改源代码,并检查是否有任何“突变体”在每个测试中存活下来。任何在单元测试中存活下来的突变体都是一个问题:这意味着对代码的修改(可能引入错误)没有被标准测试套件捕获。

mutmutPython 中用于突变测试的一个框架。

假设你需要编写代码来计算模拟时钟上时针和分针之间的夹角,精确到最接近的度数。代码可能看起来像这样

def hours_hand(hour, minutes):
    base = (hour % 12 ) * (360 // 12)
    correction = int((minutes / 60) * (360 // 12))
    return base + correction

def minutes_hand(hour, minutes):
    return minutes * (360 // 60)

def between(hour, minutes):
    return abs(hours_hand(hour, minutes) - minutes_hand(hour, minutes))

首先,编写一个简单的单元测试

import angle

def test_twelve():
    assert angle.between(12, 00) == 0

这足够了吗?代码没有 if 语句,因此如果你检查覆盖率

$ coverage run `which pytest`
============================= test session starts ==============================
platform linux -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1
rootdir: /home/moshez/src/mut-mut-test
collected 1 item                                                               

tests/test_angle.py .                                                    [100%]

============================== 1 passed in 0.01s ===============================

完美!测试通过了,覆盖率达到 100%。你是测试专家。但是,当你使用突变测试时,覆盖率看起来如何呢?

$ mutmut run --paths-to-mutate angle.py
<snip>
Legend for output:
? Killed mutants.   The goal is for everything to end up in this bucket.
⏰ Timeout.          Test suite took 10 times as long as the baseline so were killed.
? Suspicious.       Tests took a long time, but not long enough to be fatal.
? Survived.         This means your tests needs to be expanded.
? Skipped.          Skipped.
<snip>
⠋ 21/21  ? 5  ⏰ 0  ? 0  ? 16  ? 0

哦,不。在 21 个突变体中,有 16 个存活下来。只有五个案例通过了突变测试。但这意味着什么?

对于每个突变测试,mutmut 修改了你的源代码部分,以模拟潜在的错误。修改的一个例子是将 > 比较更改为 >=,以查看会发生什么。如果对于此边界条件没有单元测试,则此突变将“存活”:这是一个潜在的错误,任何测试都无法检测到。

现在是编写更好的单元测试的时候了。通过 results 可以轻松检查进行了哪些更改

$ mutmut results
<snip>
Survived ? (16)

---- angle.py (16) ----

4-7, 9-14, 16-21
$ mutmut apply 4
$ git diff
diff --git a/angle.py b/angle.py
index b5dca41..3939353 100644
--- a/angle.py
+++ b/angle.py
@@ -1,6 +1,6 @@
 def hours_hand(hour, minutes):
     hour = hour % 12
-    base = hour * (360 // 12)
+    base = hour / (360 // 12)
     correction = int((minutes / 60) * (360 // 12))
     return base + correction

这是一个典型的 mutmut 执行的突变示例;它分析源代码并将运算符更改为不同的运算符:加法更改为减法,或者在本例中,乘法更改为除法。一般来说,当运算符发生更改时,单元测试应该捕获错误;否则,它们就不能有效地测试行为。按照此逻辑,mutmut 会仔细检查源代码,以再次检查你的测试。

你可以使用 mutmut apply 来应用失败的突变体。哇,事实证明你几乎没有检查 hour 参数是否被正确使用。修复它

$ git diff
diff --git a/tests/test_angle.py b/tests/test_angle.py
index f51d43a..1a2e4df 100644
--- a/tests/test_angle.py
+++ b/tests/test_angle.py
@@ -2,3 +2,6 @@ import angle
 
 def test_twelve():
     assert angle.between(12, 00) == 0
+
+def test_three():
+    assert angle.between(3, 00) == 90

之前,你只测试了 12。添加对 3 的测试是否足以改进?

$ mutmut run --paths-to-mutate angle.py
<snip>
⠋ 21/21  ? 7  ⏰ 0  ? 0  ? 14  ? 0

这个新测试成功地杀死了两个突变体——比以前好,但仍然有很长的路要走。我不会详细介绍剩下的 14 个要修复的案例,因为我认为模式很清楚。(你能将它们减少到零吗?)

突变测试是另一种工具,与覆盖率测量一起,可以让你了解你的测试套件有多全面。使用它表明测试需要改进:任何一个存活下来的突变体都是一个人在手动输入代码时可能犯的错误,以及可能潜入你的程序中的潜在错误。继续测试,祝你狩猎愉快。

接下来阅读什么
标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
自 1998 年以来,Moshe 一直参与 Linux 社区,在 Linux “安装聚会”中提供帮助。自 1999 年以来,他一直在编写 Python 程序,并为核心 Python 解释器做出了贡献。Moshe 在这些术语出现之前就一直是 DevOps/SRE,他非常关心软件可靠性、构建可重现性以及其他此类事情。

5 条评论

不错

在“coverage run `which pytest`”命令中 100% 的输出意味着 100% 的测试被执行并通过,这与覆盖率无关。要真正显示覆盖率,请运行“coverage report”。无论如何,感谢您强调这个有用的工具。

需要与这个优秀的团队进行合作。谢谢

回复 作者:JI0C0Cb (未验证)

我早些时候学习了 PYTHON,这对我来说很困难

© . All rights reserved.