Mutation Testing 実践ガイド
作成日: 2025-12-30 対象: NexusCore エージェント開発者 ツール: mutmut v3.3.1
🎯 Mutation Testing とは
定義: テストの品質を測定する手法
コードに意図的なバグ(mutant)を注入し、テストがそのバグを検出できるかを確認します。
例
# 元のコード
def add(a, b):
return a + b
# mutant 1: + を - に変更
def add(a, b):
return a - b # バグ!
# mutant 2: + を * に変更
def add(a, b):
return a * b # バグ!
テストがこれらのバグを検出できれば「killed」、検出できなければ「survived」
🏆 目標スコア
NexusCore では 80%以上 を推奨:
🎉 17/20 killed (85%) ← 合格
🙁 3/20 survived (15%)
📦 mutmut のインストール
pip install mutmut==3.3.1
バージョン注意:
- v2.x: Python 3.11 非互換
- v3.x: 推奨(このガイドは v3.3.1 ベース)
⚙️ 設定
pyproject.toml
[tool.mutmut]
paths_to_mutate = ["src/nexuscore/agents/my_agent.py"]
tests_dir = ["tests/agents/test_my_agent.py", "-m", "not slow"]
重要ポイント:
paths_to_mutate: 対象ファイル(複数可)tests_dir: テストファイル + pytest オプション-m "not slow": 時間のかかるテストを除外
pytest.ini
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
🚀 基本的な使い方
1. mutant を生成して実行
# キャッシュをクリア
rm -rf .mutmut-cache mutants/
# 実行(シングルプロセス推奨)
mutmut run --max-children 1
出力例:
⠋ Generating mutants
done in 2405ms
⠏ Running stats
done
⠴ Running clean tests
done
⠙ Running forced fail test
done
Running mutation testing
⠸ 17/20 🎉 17 🫥 0 ⏰ 0 🤔 0 🙁 3 🔇 0
5.37 mutations/second
2. 結果を確認
# サマリー
mutmut results
# 特定の mutant を表示
mutmut show <mutant_id>
# survived した mutant のみ表示
mutmut results | grep survived
3. mutant の詳細を確認
mutmut show nexuscore.agents.my_agent.xǁMyAgentǁfunction_name__mutmut_42
出力例:
--- src/nexuscore/agents/my_agent.py
+++ [Mutant] nexuscore.agents.my_agent.xǁMyAgentǁfunction_name__mutmut_42
@@ -100,7 +100,7 @@
def calculate_score(self, total, killed):
- return (killed / total) * 100
+ return (killed - total) * 100 # ← バグ注入
📊 出力の読み方
v3.x 絵文字フォーマット
⠸ 17/20 🎉 17 🫥 0 ⏰ 0 🤔 0 🙁 3 🔇 0
| 絵文字 | 意味 | 理想値 |
|---|---|---|
| 🎉 | Killed(テストが検出) | 高いほど良い |
| 🙁 | Survived(テストが検出できず) | 低いほど良い |
| ⏰ | Timeout(テストが10倍遅い) | 0 |
| 🤔 | Suspicious(テストが遅い) | 0 |
| 🫥 | Skipped | - |
| 🔇 | Muted | - |
Mutation Score の計算
Mutation Score = (Killed / Total) * 100
= (17 / 20) * 100
= 85%
🔍 survived mutant への対応
Step 1: mutant を確認
mutmut show <mutant_id>
Step 2: なぜ検出できなかったか分析
例:
# 元のコード
def validate_score(self, score):
if score >= 80: # mutmut: >= を > に変更
return "合格"
return "不合格"
# 既存のテスト
def test_validate_score():
assert validate_score(85) == "合格" # 85 > 80 でも合格
assert validate_score(70) == "不合格"
問題: 境界値(80)をテストしていない
Step 3: テストを追加
def test_validate_score_boundary():
assert validate_score(80) == "合格" # 境界値を追加
assert validate_score(79) == "不合格"
Step 4: 再実行
mutmut run --max-children 1
⚠️ mutmut v3.x の既知の問題
“Unable to force test failures”
現象:
FAILED: Unable to force test failures
原因:
- subprocess.run などを完全にモックしているテスト
- mutmut が強制失敗テストを作れない
解決策 1: テストを改善
# ❌ 悪い例: subprocess を完全モック
with patch('subprocess.run', return_value=mock):
result = run_command()
# ✅ 良い例: 実際のコマンドを実行(またはモックを最小限に)
result = run_command() # 実際のロジックをテスト
解決策 2: 軽量なコードで検証
# simple_math.py で mutmut が動作するか確認
[tool.mutmut]
paths_to_mutate = ["simple_math.py"]
tests_dir = ["test_simple_math.py"]
解決策 3: 現状を受け入れる
- モックを使わないテスト(TestParseMutmutOutput等)で品質保証
- 実用上は十分な場合が多い
🎓 ベストプラクティス
1. テストファースト
mutation testing を意識してテストを書く:
# ✅ 境界値テスト
@pytest.mark.parametrize("score,expected", [
(79, False), # 境界値の下
(80, True), # 境界値
(81, True), # 境界値の上
])
def test_pass_threshold(score, expected):
assert check_pass(score) == expected
2. モックを最小限に
# ✅ 実際のロジックをテスト
def test_parse_output():
output = "🎉 17 🙁 3"
result = parse(output)
assert result["killed"] == 17
# ❌ モックが多すぎる
with patch('parse', return_value={"killed": 17}):
result = process() # parse が実行されない
3. テストの分離
# ✅ 高速なテスト(常に実行)
def test_parse_logic():
pass
# ✅ 遅いテスト(slowマーカー)
@pytest.mark.slow
def test_integration_with_real_mutmut():
pass # 30秒以上
4. 意味のあるテストのみ
# ❌ テストの為のテスト
def test_function_called():
with patch('func') as mock:
process()
assert mock.called # 呼ばれたかだけ
# ✅ 意味のあるテスト
def test_function_result():
result = process()
assert result == expected # 正しい結果か
📈 mutation_tester_agent の実績
Before
40 tests, 10 tests excluded from mutmut
❌ "Unable to force test failures"
After
30 tests, 1 test excluded (slow marker)
✅ simple_math.py で 100% (3/3 killed)
改善内容
- モック重視のテスト10個を削除
- TestParseMutmutOutput など実際のロジックをテスト
- pyproject.toml の除外設定を簡素化
🛠️ トラブルシューティング
Q: mutant が生成されない
A: パスが正しいか確認
# ❌ 相対パスは動作しない場合がある
paths_to_mutate = ["../src/my_agent.py"]
# ✅ プロジェクトルートからの相対パス
paths_to_mutate = ["src/nexuscore/agents/my_agent.py"]
Q: テストが実行されない
A: tests_dir の設定を確認
# ✅ 正しい
tests_dir = ["tests/agents/test_my_agent.py"]
# ❌ ディレクトリ指定は動作しない場合がある
tests_dir = ["tests/agents/"]
Q: mutation score が 0%
A: テストがコードをカバーしているか確認
# カバレッジ確認
pytest --cov=src/nexuscore/agents/my_agent tests/agents/test_my_agent.py
🔗 関連リソース
📝 クイックリファレンス
# セットアップ
pip install mutmut==3.3.1
# 実行
rm -rf .mutmut-cache mutants/
mutmut run --max-children 1
# 結果確認
mutmut results
mutmut results | grep survived
mutmut show <mutant_id>
# テスト(slowマーカー除外)
pytest -m "not slow"
# simple_math.py で動作確認
# pyproject.toml を一時的に変更
mutmut run --max-children 1
📝 更新履歴
- 2025-12-30: 初版作成(mutation_tester_agent での経験を基に)