mutmut
Pythonでミューテーションテストを行うライブラリ。
コードを微妙に変えながらテストを繰り返し、境界値テストが不完全な箇所を洗い出すツール
https://scrapbox.io/files/60fa457193682a002221d20f.png
https://pypi.org/project/mutmut/
実行結果
code:pytest
(venv) taka@SHIMIZUKAWA-X1C2018:~/mutmut$ pytest -q
....... 100%
------------------------------ generated html file: file:///home/taka/mutmut/report.html -------------------------------
----------- coverage: platform linux, python 3.6.9-final-0 -----------
Coverage HTML written to dir htmlcov
7 passed in 2.91s
code:mutmut run(console)
(venv) taka@SHIMIZUKAWA-X1C2018:~/mutmut$ mutmut run
- Mutation testing starting -
These are the steps:
1. A full test suite run will be made to make sure we
can run the tests successfully and we know how long
it takes (to detect infinite loops for example)
2. Mutants will be generated and checked
Results are stored in .mutmut-cache.
Print found mutants with mutmut results.
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.
1. Running tests without mutations
⠼ Running...Done
2. Checking mutants
⠋ 8/8 🎉 5 ⏰ 0 🤔 0 🙁 3 🔇 0
code:mutmut show all
(venv) taka@SHIMIZUKAWA-X1C2018:~/mutmut$ mutmut show all
To apply a mutant on disk:
mutmut apply <id>
To show a mutant:
mutmut show <id>
Survived 🙁 (3)
---- src/code.py (3) ----
# mutant 3
--- src/code.py
+++ src/code.py
@@ -1,7 +1,7 @@
def get_price(price: int, age: int, is_old: bool):
if is_old:
return min(price, 100)
- if not(18 <= age < 60):
+ if not(19 <= age < 60):
return price // 2
return price
# mutant 4
--- src/code.py
+++ src/code.py
@@ -1,7 +1,7 @@
def get_price(price: int, age: int, is_old: bool):
if is_old:
return min(price, 100)
- if not(18 <= age < 60):
+ if not(18 < age < 60):
return price // 2
return price
# mutant 7
--- src/code.py
+++ src/code.py
@@ -2,6 +2,6 @@
if is_old:
return min(price, 100)
if not(18 <= age < 60):
- return price // 2
+ return price / 2
return price
テスト対象コードとテスト
code:src/code.py
def get_price(price: int, age: int, is_old: bool):
if is_old:
return min(price, 100)
if not(18 <= age < 60):
return price // 2
return price
code:tests/test_code.py
import pytest
@pytest.fixture
def target():
import code
return code.get_price
@pytest.mark.parametrize(
"price,age,is_old,expected", [
(400, 10, False, 200),
(400, 20, False, 400),
(400, 60, False, 200),
(400, 10, True, 100),
(400, 20, True, 100),
(400, 60, True, 100),
(50, 20, True, 50),
])
def test_get_price(target, price, age, is_old, expected):
actual = target(price, age, is_old)
assert expected == actual
テスト対象の環境
code:consle
(venv) taka@SHIMIZUKAWA-X1C2018:~/mutmut$ tree -I venv
.
├── requirements.lock
├── requirements.txt
├── setup.cfg
├── src
│   └── code.py
└── tests
└── test_code.py
code:requirements.txt
pytest
pytest-pythonpath
pytest-cov
pytest-html
mutmut
code:requirements.lock
attrs==21.2.0
click==8.0.1
coverage==5.5
glob2==0.7
importlib-metadata==4.6.1
iniconfig==1.1.1
junit-xml==1.8
mutmut==2.1.0
packaging==21.0
parso==0.8.2
pkg-resources==0.0.0
pluggy==0.13.1
pony==0.7.14
py==1.10.0
pyparsing==2.4.7
pytest==6.2.4
pytest-cov==2.12.1
pytest-html==3.1.1
pytest-metadata==1.11.0
pytest-pythonpath==0.7.3
six==1.16.0
toml==0.10.2
typing-extensions==3.10.0.0
zipp==3.5.0
code:setup.cfg
tool:pytest
addopts = --cov --cov-report=html --html=report.html --self-contained-html
python_paths = src/
testpaths = tests/
mutmut
paths_to_mutate = src/
tests_dir = tests/
backup = True
runner = pytest
dict_synonyms = Struct, NamedStruct