Mutation Testing サーベイ — Go, Rust, Python, TypeScript の主要ツール比較

この記事はAIによって生成されています。内容の正確性にご注意ください。

TL;DR

  • Mutation testingはコードに小さな変異(mutant)を注入し、テストがそれを検出できるかを検証する手法。コードカバレッジでは測れない「テストの質」を定量化できる
  • 各言語の推奨ツール: Python → mutmut, TypeScript → StrykerJS, Rust → cargo-mutants, Go → Gremlins
  • すべてのツールが差分ベースの実行量の削減に対応している(またはワークアラウンドがある)。CIに組み込む際は差分ベースの実行が実質必須
  • 言語の型システムの強さがmutation testingの効率に直結する。Rustのような強い型システムでは無効なmutantが型チェックで自動排除され、実行時間が短くなる

ツール一覧と差分実行・並列実行の対応状況

ツール言語差分実行並列実行
boxed/mutmut⭐ 1.2kupdated: 2026-03-23Python○ 組み込みインクリメンタル + --paths-to-mutateで対象指定× 逐次実行のみ
stryker-mutator/stryker-js⭐ 2.8kupdated: 2026-03-24TypeScript--incrementalでJSON差分管理
sourcefrog/cargo-mutants⭐ 1.1kupdated: 2026-03-07Rust--in-diffでdiffファイル指定-jフラグ
go-gremlins/gremlins⭐ 306updated: 2026-03-23Go△ git diffとの組み合わせでワークアラウンド

Mutation Testing とは

コードカバレッジは「テストがどの行を実行したか」を示す指標ですが、テストが実際にバグを検出できるかは別問題です。カバレッジ100%でも、assertionが1つもなければバグは見つかりません。

Mutation testingはこの問題に対するアプローチで、以下のように動作します。

  1. ソースコードに小さな変異(mutation)を加えたmutantを生成する
  2. 各mutantに対してテストスイートを実行する
  3. テストが失敗すればkilled(検出成功)、全テストが通ればsurvived(検出失敗)

最終的に、killed mutantの割合であるMutation Scoreがテストスイートの品質指標となります。

Mutation Score=Killed MutantsTotal MutantsEquivalent Mutants×100\text{Mutation Score} = \frac{\text{Killed Mutants}}{\text{Total Mutants} - \text{Equivalent Mutants}} \times 100

代表的なMutation Operator

種類変異前変異後
算術演算子a + ba - b
比較演算子a > ba >= b, a < b
論理演算子a && ba || b
戻り値return xreturn 0, return ""
条件式if (cond)if (true), if (false)
文の削除doSomething()(削除)

Equivalent Mutant問題

Mutation testingの根本的な課題として、equivalent mutantがあります。これは変異を加えてもプログラムの外部から観察可能な動作が変わらないmutantのことで、原理的にテストでは検出できません。例えば x = x * 1x = x * 0 に変えた場合はkillできますが、 x = x + 0x = x - 0 に変えた場合は動作が同一のため検出できません。この問題は決定不能であることが知られており、各ツールはヒューリスティクスで対処しています。

Python: mutmut

boxed/mutmut⭐ 1.2kupdated: 2026-03-23はPythonにおける最も広く使われているmutation testingツールです。使いやすさを重視した設計で、pytestとの統合がスムーズに行えます。

セットアップと実行例

pip install mutmut

pyproject.tomlで設定を記述します。

pyproject.toml

[tool.mutmut]
paths_to_mutate = "src/"
tests_dir = "tests/"

対象となるコードとテストの例を示します。

src/calculator.py

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

tests/test_calculator.py

import pytest
from src.calculator import divide

def test_divide_normal():
    assert divide(10, 2) == 5.0

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(10, 0)

mutation testingを実行します。

mutmut run --paths-to-mutate src/calculator.py

結果を確認します。

mutmut results

survivedしたmutantがあれば、以下のようにdiffを確認できます。

mutmut show <mutant_id>

例えば b == 0b == 1 に変異したmutantがsurvivedしていた場合、b=1のケースに対するテストが不足していることがわかります。

テストフレームワーク連携

mutmutはデフォルトでpytestを使用します。unittestや他のランナーを使う場合はrunner設定で指定できます。

pytestとの連携では、カバレッジ情報を活用した最適化が可能です。mutate_only_covered_linestrueにすると、テストでカバーされている行のみを変異対象にできます。

pyproject.toml

[tool.mutmut]
paths_to_mutate = "src/"
tests_dir = "tests/"
mutate_only_covered_lines = true

また、mypypyreflyといった型チェッカーとの連携もサポートしています。x: str = 'foo'x: str = Noneに変異させた場合、型チェッカーが検出できるため無駄な実行を省けます。

差分ベースの実行量の削減

mutmutはインクリメンタル実行を組み込みでサポートしています。

  • 実行を途中で停止しても、次回は中断した箇所から再開する
  • ソースコードが変更された関数のみを再テストする
  • テストスイートが変更された場合、survivedしたmutantのみを再テストする

CIでの利用では、--paths-to-mutateにgit diffで取得した変更ファイルを渡すことで、変更箇所のみを対象にできます。

# 変更されたPythonファイルのみを対象にする
git diff --name-only origin/main -- '*.py' | xargs -I {} mutmut run --paths-to-mutate {}

TypeScript: StrykerJS

stryker-mutator/stryker-js⭐ 2.8kupdated: 2026-03-24はJavaScript/TypeScriptエコシステムにおけるデファクトのmutation testingツールです。活発にメンテナンスされており、v7.0ではVitest対応が追加されました。

セットアップと実行例

npm init stryker
# または手動インストール
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner @stryker-mutator/typescript-checker

設定ファイルを作成します。

stryker.config.json

{
  "$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/core/schema/stryker-schema.json",
  "testRunner": "vitest",
  "checkers": ["typescript"],
  "tsconfigFile": "tsconfig.json",
  "mutate": ["src/**/*.ts", "!src/**/*.test.ts"]
}

対象コードとテストの例を示します。

src/calculator.ts

export function clamp(value: number, min: number, max: number): number {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

src/calculator.test.ts

import { describe, it, expect } from "vitest";
import { clamp } from "./calculator";

describe("clamp", () => {
  it("returns min when value is below range", () => {
    expect(clamp(-5, 0, 10)).toBe(0);
  });

  it("returns max when value is above range", () => {
    expect(clamp(15, 0, 10)).toBe(10);
  });

  it("returns value when within range", () => {
    expect(clamp(5, 0, 10)).toBe(5);
  });
});

実行します。

npx stryker run

StrykerはHTMLレポートを生成し、各mutantのstatus(killed/survived/no coverage/compile error)を視覚的に確認できます。

テストフレームワーク連携

StrykerJSは豊富なテストランナープラグインを提供しています。

ランナーパッケージ備考
Vitest@stryker-mutator/vitest-runnerv7.0で追加。推奨
Jest@stryker-mutator/jest-runnerReact/Next.js向け
Mocha@stryker-mutator/mocha-runnerNode.js向け
Karma@stryker-mutator/karma-runnerAngular向け
Node Tap@stryker-mutator/tap-runnerv7.0で追加

TypeScript Checkerプラグインは型エラーとなるmutantを自動的に除外します。例えばnumber型の戻り値を""(空文字列)に変異させた場合、コンパイルエラーとなるため実行がスキップされます。v6.4でパフォーマンスが最大50%改善されました。

差分ベースの実行量の削減(Incremental Mode)

StrykerJSのincremental modeは、mutation testingの実行量を削減する仕組みとして最も洗練されたものの1つです。

npx stryker run --incremental

動作の流れは以下の通りです。

  1. 初回は通常通り全mutantをテストし、結果をreports/stryker-incremental.jsonに保存する
  2. 2回目以降は保存済みのJSONを読み込み、ソースコードとテストファイルのdiffを算出する
  3. 変更されたコードに関連するmutantのみ再実行し、それ以外は前回の結果を再利用する
  4. 部分実行でも、全mutantの結果を含むレポートが生成される

CIでの推奨パターンは、incremental JSONファイルをCIアーティファクトとして管理する方法です。

# mainブランチの結果を取得してincremental実行
npx stryker run --incremental

特定のファイルのみを再テストしたい場合は--force--mutateを組み合わせます。

npx stryker run --incremental --force --mutate src/calculator.ts

テスト変更の検出レベル

incrementalモードにおけるテスト変更検出の精度はランナーによって異なります。

  • Jest/Mocha/Cucumberではテストの正確な位置まで追跡でき、変更されたテストのみ再実行できる
  • Vitestではテストファイル単位での検出となり、ファイル内の変更があれば全テストが再実行される
  • Karmaではテスト名のみの追跡となり、追加・削除のみ検出可能

Rust: cargo-mutants

sourcefrog/cargo-mutants⭐ 1.1kupdated: 2026-03-07はRust向けのmutation testingツールで、ゼロコンフィグで動作する点が特徴です。cargo testをそのまま利用するため、既存のテスト環境に追加の設定なしで導入できます。

セットアップと実行例

cargo install cargo-mutants

対象コードの例を示します。

src/lib.rs

pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fibonacci_base_cases() {
        assert_eq!(fibonacci(0), 0);
        assert_eq!(fibonacci(1), 1);
    }

    #[test]
    fn test_fibonacci_recursive() {
        assert_eq!(fibonacci(10), 55);
    }
}

実行します。

cargo mutants

出力例を示します。

Found 8 mutants to test
  Caught   fibonacci: replace fibonacci -> u64 with 0
  Caught   fibonacci: replace fibonacci -> u64 with 1
  Caught   fibonacci: replace == with != in fibonacci
  Caught   fibonacci: replace + with - in fibonacci
  ...
8 mutants tested: 8 caught

テストフレームワーク連携

cargo-mutantsはcargo testをそのまま実行するため、標準のテストフレームワークはもちろん、#[tokio::test]#[sqlx::test]などのカスタムテスト属性も自動的に認識します。テスト関数自体は変異対象から除外されます。

nextestを使用している場合は--test-tool=nextestで切り替えが可能です。

cargo mutants --test-tool=nextest

Rustの強い型システムはmutation testingにおいて大きな利点となります。例えばOption<T>を返す関数に対して0を返すmutantを生成しても型エラーでコンパイルが通らないため、自動的にスキップされます。これにより、実行が必要なmutant数が他の言語と比較して少なくなる傾向があります。

差分ベースの実行量の削減(--in-diff

cargo-mutantsは--in-diffオプションでdiffファイルを受け取り、変更された箇所に関連するmutantのみを実行できます。

# PRの差分のみをテスト
git diff origin/main.. > pr.diff
cargo mutants --in-diff pr.diff

GitHub Actionsでの利用例を示します。

.github/workflows/mutants.yml

name: Mutation Testing
on: pull_request

jobs:
  mutants:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: cargo install cargo-mutants
      - run: git diff origin/${{ github.base_ref }}.. > git.diff
      - run: cargo mutants --no-shuffle -vV --in-diff git.diff

内部的には、cargo-mutantsはソースツリーをスクラッチディレクトリにコピーし、同じディレクトリを全mutantで再利用することでインクリメンタルビルドの恩恵を受けています。

Go: Gremlins

Go向けのmutation testingツールは複数存在しますが、現在最も活発にメンテナンスされているのはgo-gremlins/gremlins⭐ 306updated: 2026-03-23です。zimmski/go-mutesting⭐ 664updated: 2024-07-04(およびそのAvito fork)も選択肢ですが、Gremlinsの方が機能面で充実しています。

セットアップと実行例

go install github.com/go-gremlins/gremlins/cmd/gremlins@latest

対象コードの例を示します。

pkg/math/math.go

package math

func Max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

pkg/math/math_test.go

package math

import "testing"

func TestMax(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 2},
        {3, 1, 3},
        {5, 5, 5},
    }
    for _, tt := range tests {
        if got := Max(tt.a, tt.b); got != tt.want {
            t.Errorf("Max(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

実行します。

gremlins unleash ./pkg/math/...

テストフレームワーク連携

Gremlinsはgo testを直接利用します。Goにはサードパーティのテストフレームワーク(testifyなど)がありますが、いずれもgo test経由で実行されるため、追加の設定は不要です。

Gremlinsの特徴として、カバレッジ情報を事前に収集し、テストでカバーされているコードのみを変異対象とします。カバーされていないコードはそもそもテストが存在しないため、mutation testingを行っても意味がないという合理的なアプローチです。

差分ベースの実行量の削減

Gremlins自体にはStrykerJSのようなincrementalモードや、cargo-mutantsのような--in-diffオプションは現時点で組み込まれていません。

v0.6.0で追加された--exclude-filesフラグを活用し、git diffと組み合わせることでワークアラウンドが可能です。

# 変更されたGoファイルのディレクトリのみを対象にする
CHANGED_DIRS=$(git diff --name-only origin/main -- '*.go' | xargs -I {} dirname {} | sort -u | sed 's|$|/...|')
gremlins unleash $CHANGED_DIRS

go-mutestingの方はターゲットを引数で柔軟に指定でき、ファイル単位、ディレクトリ単位、パッケージ単位での実行が可能です。

どのプロジェクトで導入すべきか

向いているケース

  • ビジネスロジックが集中する小〜中規模モジュール。関数の入出力が明確で、mutation operatorが効果的に働く
  • 既にテストカバレッジが高いプロジェクト。カバレッジは高いがテストの質に自信がない場合、mutation testingが具体的な改善箇所を示してくれる
  • CIで品質ゲートを設けたい場合。差分ベースの実行でPR単位のチェックが実用的になっている

向かないケース

  • テストカバレッジが低いプロジェクト。まずカバレッジを上げる方が費用対効果が高い
  • 巨大なモノリス。mutant数が爆発し、実行時間が非現実的になる。モジュール分割してから導入すべき
  • UI/E2Eテスト中心のプロジェクト。mutation testingはユニットテスト・インテグレーションテストとの相性が良い

導入時の注意点

  • 全mutantのテストは時間がかかる。まずは差分ベースの実行から始め、mainブランチでの定期的なフル実行と組み合わせるのが現実的
  • Mutation Scoreは100%を目指す必要はない。equivalent mutantの存在もあり、80%程度を目安にするのが妥当
  • 最初からプロジェクト全体に適用せず、重要なモジュールから段階的に始める

まとめ

Mutation testingは「テストの質をテストする」手法であり、4言語ともツールが存在します。ツールの成熟度にはばらつきがあり、StrykerJS(TypeScript)とmutmut(Python)がエコシステムとして最も充実しています。cargo-mutants(Rust)はRustの型システムとの親和性が高く、ゼロコンフィグで始められる手軽さがあります。Go向けのGremlinsはまだ0.x系ですが、カバレッジベースのフィルタリングなど実用的な機能を備えています。

CIへの組み込みにおいては、差分ベースの実行(StrykerJSの--incremental、cargo-mutantsの--in-diff、mutmutの--paths-to-mutate)が実質的に必須です。フル実行はmainブランチでの定期実行に留め、PRでは変更箇所のみを対象にするのが実用的なアプローチです。

References

この記事に関するIssueをGithubで作成する

Read Next