Merge pull request #20292 from wxy/20200720-An-introduction-to-mutation-testing-in-Python

PRF&PUB:20200720 An introduction to mutation testing in Python
This commit is contained in:
Xingyu.Wang 2020-11-29 23:08:16 +08:00 committed by GitHub
commit 03e4f8661f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,28 +1,31 @@
[#]: collector: (lujun9972)
[#]: translator: (MjSeven)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: reviewer: (wxy)
[#]: publisher: (wxy)
[#]: url: (https://linux.cn/article-12871-1.html)
[#]: subject: (An introduction to mutation testing in Python)
[#]: via: (https://opensource.com/article/20/7/mutmut-python)
[#]: author: (Moshe Zadka https://opensource.com/users/moshez)
Python 突变测试
Python 突变测试介
======
通过突变测试来修复未知的 bug。
![Searching for code][1]
> 通过突变测试来修复未知的 bug。
![](https://img.linux.net.cn/data/attachment/album/202011/29/230106ie9xc89dj3jx1yj9.jpg)
你一定对所有内容都进行了测试,也许你甚至在项目仓库中有一个徽章,标明有 100% 的测试覆盖率,但是这些测试真的帮到你了吗?你怎么知道的?
开发人员很清楚单元测试的 _成本_,必须编写测试。有时它们无法按照预期工作:存在假告警或者抖动测试。在不更改任何代码的情况下有时成功,有时失败。通过单元测试发现的小问题很有价值,但是通常它们悄无声息的出现在开发人员的机器上,并且在提交到版本控制之前就已得到修复。但真正令人担忧的问题大多是不可见的。最糟糕的是_丢失的告警_ 是完全不可见的:你看不到没能捕获的错误,直到出现在用户手上--有时甚至连用户都看不到。
开发人员很清楚单元测试的*成本*。测试必须要编写。有时它们无法按照预期工作:存在假告警或者抖动测试。在不更改任何代码的情况下有时成功,有时失败。通过单元测试发现的小问题很有价值,但是通常它们悄无声息的出现在开发人员的机器上,并且在提交到版本控制之前就已得到修复。但真正令人担忧的问题大多是看不见的。最糟糕的是,*丢失的告警*是完全不可见的:你看不到没能捕获的错误,直到出现在用户手上 —— 有时甚至连用户都看不到。
有一种测试可以使不可见的错误变为可见:[突变测试][2]。
有一种测试可以使不可见的错误变为可见:<ruby>[突变测试][2]<rt>mutation testing</rt></ruby>
变异测试通过算法修改源代码,并检查每次测试是否都有“变异体”存活。任何在单元测试中幸存下来的变异体都是问题:这意味着对代码的修改(可能会引入错误)没有被标准测试套件捕获。
[Python][3] 中用于突变测试的一个框架是 `mutmut`
假设你需要编写代码来计算钟表中时针和分针之间的角度,直到最接近的度数,代码可能会这样写:
```
def hours_hand(hour, minutes):
    base = (hour % 12 ) * (360 // 12)
@ -37,6 +40,7 @@ def between(hour, minutes):
```
首先,写一个简单的单元测试:
```
import angle
@ -45,6 +49,7 @@ def test_twelve():
```
足够了吗?代码没有 `if` 语句,所以如果你查看覆盖率:
```
$ coverage run `which pytest`
============================= test session starts ==============================
@ -58,71 +63,74 @@ tests/test_angle.py .                                      
```
完美!测试通过,覆盖率为 100%,你真的是一个测试专家。但是,当你使用突变测试时,覆盖率会变成多少?
```
$ mutmut run --paths-to-mutate angle.py
&lt;snip&gt;
<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.
&lt;snip&gt;
⠋ 21/21  🎉 5  ⏰ 0  🤔 0  🙁 16  🔇 0
🎉 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 个存活。只有 5 个通过了突变测试,但是,这意味着什么呢?
天啊,在 21 个突变体中,有 16 个存活。只有 5 个通过了突变测试,但是,这意味着什么呢?
对于每个突变测试,`mutmut` 会修改部分源代码,以模拟潜在的错误,修改的一个例子是将 `>` 比较更改为 `>=`,查看会发生什么。如果没有针对这个边界条件的单元测试,那么这个突变将会“存活”:这是一个没有任何测试用例能够检测到的潜在错误。
是时候编写更好的单元测试了。很容易检查使用 `results` 所做的更改:
```
$ mutmut results
&lt;snip&gt;
<snip>
Survived 🙁 (16)
\---- angle.py (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
--- 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
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
--- 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_twelve():
assert angle.between(12, 00) == 0
+
+def test_three():
\+    assert angle.between(3, 00) == 90
+ assert angle.between(3, 00) == 90
```
以前,你只测试了 12 点钟,现在增加一个 3 点钟的测试就足够了吗?
```
$ mutmut run --paths-to-mutate angle.py
&lt;snip&gt;
⠋ 21/21  🎉 7  ⏰ 0  🤔 0  🙁 14  🔇 0
<snip>
⠋ 21/21 🎉 7 ⏰ 0 🤔 0 🙁 14 🔇 0
```
这项新测试成功杀死了两个突变体,比以前更好,当然还有很长的路要走。我不会一一解决剩下的 14 个测试用例,因为我认为模式已经很明确了。(你能将它们降低到零吗?)
@ -136,7 +144,7 @@ via: https://opensource.com/article/20/7/mutmut-python
作者:[Moshe Zadka][a]
选题:[lujun9972][b]
译者:[MjSeven](https://github.com/MjSeven)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出