翻译完成

This commit is contained in:
MjSeven 2020-11-22 00:00:46 +08:00
parent cd9632dd20
commit be6e59c65c
2 changed files with 147 additions and 160 deletions

View File

@ -1,160 +0,0 @@
[#]: collector: (lujun9972)
[#]: translator: (MjSeven)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: 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)
An introduction to mutation testing in Python
======
Turn invisible bugs into visible fixes with mutation testing.
![Searching for code][1]
You have tests for everything; maybe you even have a badge in your project repository stating 100% test coverage. But what are these tests helping you do? How do you know?
The _costs_ of unit tests are clear to developers. Tests have to be written. Occasionally they don't work as intended: there are false alarms or flapping tests that alternate success and failure without any code changes. The small bugs you can find through unit tests are valuable, but often they happen quietly on a developer machine and are fixed before a commit ever goes into version control. But the truly alarming bugs are mostly invisible. And the worst of all, the _missing alarms_ are completely invisible: you don't see the bugs you failed to catch until the code gets into a user's hands—and sometimes not even then.
There is one type of test that makes it possible to make the invisible visible: [mutation testing][2].
Mutation testing algorithmically modifies source code and checks if any "mutants" survived each test. Any mutant that survives the unit test is a problem: it means that a modification to the code, likely introducing a bug, was not caught by the standard test suite.
One framework for mutation testing in [Python][3] is `mutmut`.
Imagine you need to write code that calculates the angle between the hour hand and the minute hand in an analog clock to the nearest degree. The code might look something like:
```
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))
```
To begin, write a simple unit test:
```
import angle
def test_twelve():
    assert angle.between(12, 00) == 0
```
Is this enough? The code has no `if` statement, so if you check coverage:
```
$ 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 ===============================
```
Perfect! The tests pass, and coverage is at 100%. Testing expert, you are. But what does that coverage look like when you use mutation testing?
```
$ 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
```
Oh no. Out of 21 mutants, 16 survived. Only five cases passed the mutation testing. But what does that mean?
For each mutation test, `mutmut` modified portions of your source code that simulates potential bugs. An example of a modification is changing a `>` comparison to `>=` to see what happens. If there is no unit test for this boundary condition, this mutation will "survive": this is a potential bug that none of the tests will detect.
It's time to write better unit tests. It is easy to check which changes were made with `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
```
This is a typical example of a mutation `mutmut` performs; it analyzes the source code and changes operators to different operators: addition to subtraction or, in this case, multiplication to division. Unit tests, generally speaking, should catch bugs when an operator changes; otherwise, they are not testing the behavior effectively. Following this logic, `mutmut` twiddles through source code to double-check your tests.
You can use `mutmut apply` to apply a failed mutant. Wow, it turns out you barely checked whether the `hour` parameter was used correctly. Fix that:
```
$ 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
```
Previously, you tested only for 12. Will adding a test for three be enough to improve?
```
$ mutmut run --paths-to-mutate angle.py
<snip>
⠋ 21/21  🎉 7  ⏰ 0  🤔 0  🙁 14  🔇 0
```
This new test managed to kill two of the mutants—better than before, but still a long way to go. I won't work through each of the 14 cases left to fix, because I think the pattern is clear. (Can you get them down to zero?)
Mutation testing is another tool, along with coverage measurement, that allows you to see how comprehensive your test suite is. Using it makes the case that tests need to be improved: any one of those surviving mutants is a mistake a human being can make while fat-fingering code, as well as a potential bug creeping into your program. Keep testing and happy hunting.
--------------------------------------------------------------------------------
via: https://opensource.com/article/20/7/mutmut-python
作者:[Moshe Zadka][a]
选题:[lujun9972][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/moshez
[b]: https://github.com/lujun9972
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/search_find_code_python_programming.png?itok=ynSL8XRV (Searching for code)
[2]: https://opensource.com/article/19/8/mutation-testing-evolution-tdd
[3]: https://opensource.com/resources/python

View File

@ -0,0 +1,147 @@
[#]: collector: (lujun9972)
[#]: translator: (MjSeven)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: 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 突变测试简介
======
通过突变测试来修复未知的 bug。
![Searching for code][1]
你一定对所有内容都进行了测试,也许你甚至在项目仓库中有一个徽章,标明有 100% 的测试覆盖率,但是这些测试真的帮到你了吗?你怎么知道的?
开发人员很清楚单元测试的 _成本_必须编写测试。有时它们无法按照预期工作存在假告警或者抖动测试。在不更改任何代码的情况下有时成功有时失败。通过单元测试发现的小问题很有价值但是通常它们悄无声息的出现在开发人员的机器上并且在提交到版本控制之前就已得到修复。但真正令人担忧的问题大多是不可见的。最糟糕的是_丢失的告警_ 是完全不可见的:你看不到没能捕获的错误,直到出现在用户手上--有时甚至连用户都看不到。
有一种测试可以使不可见的错误变为可见:[突变测试][2]。
变异测试通过算法修改源代码,并检查每次测试是否都有“变异体”存活。任何在单元测试中幸存下来的变异体都是问题:这意味着对代码的修改(可能会引入错误)没有被标准测试套件捕获。
[Python][3] 中用于突变测试的一个框架是 `mutmut`
假设你需要编写代码来计算钟表中时针和分针之间的角度,直到最接近的度数,代码可能会这样写:
```
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 个存活。只有 5 个通过了突变测试,但是,这意味着什么呢?
对于每个突变测试,`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 个测试用例,因为我认为模式已经很明确了。(你能将它们降低到零吗?)
变异测试和覆盖率一样,是一种工具,它允许你查看测试套件的全面程度。使用它使得测试用例需要改进:那些幸存的突变体中的任何一个都是人类在篡改代码时可能犯的错误,以及潜伏在程序中的隐藏错误。继续测试,愉快地搜寻 bug 吧。
--------------------------------------------------------------------------------
via: https://opensource.com/article/20/7/mutmut-python
作者:[Moshe Zadka][a]
选题:[lujun9972][b]
译者:[MjSeven](https://github.com/MjSeven)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/moshez
[b]: https://github.com/lujun9972
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/search_find_code_python_programming.png?itok=ynSL8XRV (Searching for code)
[2]: https://opensource.com/article/19/8/mutation-testing-evolution-tdd
[3]: https://opensource.com/resources/python