0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Stryker・PIT・mutmutで始めるミューテーションスコアの可視化と品質トレンド分析

0
Last updated at Posted at 2026-05-08

Stryker・PIT・mutmutで始めるミューテーションスコアの可視化と品質トレンド分析

この記事でわかること

  • ミューテーションテストの8つのミュータント状態とメトリクス計算式の正確な理解
  • Stryker(TypeScript)・PIT(Java)・mutmut(Python)の3ツールでレポートを生成し、可視化する手順
  • ミューテーションスコアをCI/CDパイプラインで時系列記録し、Grafanaダッシュボードでトレンド分析する方法
  • モジュール・チーム横断での品質比較と、テスト改善の優先度を判断する実践パターン
  • カバレッジ率では発見できないテストの弱点を、ミューテーションスコアで特定・改善した事例

対象読者

  • 想定読者: テストカバレッジの数字に疑問を感じているMLエンジニア・ソフトウェアエンジニア
  • 必要な前提知識:
    • Python / TypeScript / Java いずれかの基礎文法
    • pytest / Vitest / JUnit などのテストフレームワークの基本的な使い方
    • CI/CDパイプライン(GitHub Actions等)の基本理解
    • MLエンジニアの方へ: pytestでの単体テスト経験があれば十分です。TypeScript/Javaのセクションはコード例にPythonの類似概念をコメントで補足しています

結論・成果

ミューテーションテストの結果を可視化し、時系列でトレンド分析することで、テストスイートのバグ検出能力を定量的かつ継続的にモニタリングできるようになります。Sentryの事例では、StrykerJS導入後にCore SDKのミューテーションスコア62%を計測し、カバレッジでは見えなかったテストの弱点を特定したと公式ブログで報告しています。また、Metaの事例では、LLMベースのミューテーション生成(ACHシステム)を10,795のKotlinクラスに適用し、9,095個のミュータントと571個のテストケースを生成、プライバシーエンジニアによるテスト受入率は73%に達したとEngineering Blogで報告されています。

本記事では、ツール標準のHTMLレポートからカスタムダッシュボードによるチーム横断の品質トレンド分析まで、可視化の3段階を実践的に解説します。

ミューテーションテストのCI自動化や品質ゲート設計については、関連記事「Stryker・cargo-mutantsで実践するミューテーションテストCI自動化」も参照してください。

ミューテーションテストのメトリクス体系を理解する

ミューテーションテストの可視化を始める前に、何を計測し、何を可視化するのかを正確に理解しておく必要があります。ここではStrykerのメトリクス定義に基づいて、8つのミュータント状態とメトリクス計算式を整理します。

8つのミュータント状態を把握する

ミュータント(コードに意図的に埋め込まれた変異)は、テスト実行後に以下の8状態のいずれかに分類されます。

状態 意味 メトリクス上の分類
Killed テストが変異を検出し、失敗した Detected
Timeout テスト実行がタイムアウトした Detected
Survived 全テストがパスした(変異を検出できず) Undetected
No Coverage テストがそもそもカバーしていない Undetected
Runtime Error 実行時エラーが発生 Invalid(スコアから除外)
Compile Error コンパイルエラーが発生 Invalid(スコアから除外)
Ignored 意図的にスキップされた 除外
Pending 未実行(処理中の一時状態) 除外

MLエンジニアの方は、Killed/Survivedを機械学習の混同行列(Confusion Matrix)に置き換えると理解しやすいかもしれません。Killedは「バグを正しく検出できた」(True Positive)、Survivedは「バグを見逃した」(False Negative)に対応します。

メトリクス計算式を確認する

Strykerのドキュメントでは、以下の計算式が定義されています。

Detected    = Killed + Timeout
Undetected  = Survived + No Coverage
Valid       = Detected + Undetected
Invalid     = Runtime Errors + Compile Errors

Mutation Score         = Detected / Valid × 100
Coverage-Based Score   = Detected / (Detected + Survived) × 100

Mutation Score(ミューテーションスコア)は「有効なミュータントのうち、テストで検出できた割合」を示します。Coverage-Based Score はカバレッジのあるコード内での検出率を示し、No Coverageのミュータントを除外した指標です。

注意点: この2つのスコアの差が大きい場合、テストカバレッジ自体が不足しています。たとえばMutation Score = 50%、Coverage-Based Score = 90%であれば、カバーされたコードのテスト品質は高いが、カバーされていないコードが多いことを意味します。

カバレッジ率とミューテーションスコアの違いを理解する

「カバレッジ100%なのにバグが出る」という現象は、多くのエンジニアが経験しているでしょう。カバレッジはコードが実行されたことを示しますが、テストが正しく検証したことは保証しません。

# tests/test_discount.py
# カバレッジ100%だがミューテーションテストで弱点が露呈する例

def calculate_discount(price: int, is_member: bool) -> int:
    """会員なら10%割引を適用する"""
    if is_member:
        return int(price * 0.9)
    return price


def test_discount_member():
    # カバレッジ的には is_member=True のブランチを通過 → カバレッジ貢献
    result = calculate_discount(1000, True)
    # しかしアサーションがない! ミュータント "price * 0.9 → price * 0.0" が生き残る
    assert result is not None  # 弱いアサーション


def test_discount_non_member():
    result = calculate_discount(1000, False)
    assert result == 1000  # 正確なアサーション → ミュータントを殺せる

上記の test_discount_member はカバレッジには貢献しますが、0.90.0 に変異させても result is not None は引き続きパスします。ミューテーションテストはこのアサーションの甘さを検出します。

修正後のテストでは、ミュータントを確実にKillできます。

def test_discount_member_fixed():
    result = calculate_discount(1000, True)
    assert result == 900  # 具体的な期待値 → ミュータントを殺せる

よくある間違い: 「カバレッジが高ければテスト品質は十分」と考えてしまうことです。実際にはカバレッジ100%でもミューテーションスコアが50%以下ということは珍しくありません。カバレッジはテストの量、ミューテーションスコアはテストの質を測る指標だと理解してください。

3つのツールでミューテーションテストレポートを生成する

ここでは、Python・TypeScript・Javaの3言語について、それぞれの主要ツールでレポートを生成する手順を解説します。自分の言語環境に合ったセクションを参照してください。

mutmut(Python)でHTMLレポートを生成する

mutmutはPython向けのミューテーションテストツールで、使いやすさに重点を置いています。mutmut 2.5.0(2025年安定版)を使用します。

# インストール
pip install mutmut==2.5.0

# ミューテーションテスト実行
# --paths-to-mutate でテスト対象を指定
mutmut run --paths-to-mutate src/

# 結果の確認(テキスト)
mutmut results

# HTMLレポート生成
mutmut html
# → htmlcov/ ディレクトリにレポートが生成される

mutmutの出力は以下のような形式です。

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 killed by timeout.
🙁 Survived.         This means your tests need to be expanded.
🔇 Skipped.          Skipped.

--- Results ---
Total: 142
Killed: 98 (69%)
Survived: 32 (23%)
Timeout: 8 (6%)
Suspicious: 4 (3%)

なぜmutmutか:

  • Pythonエコシステムでの採用率が高く、ドキュメントが充実している
  • インクリメンタル実行をサポートし、前回の結果をキャッシュできる
  • Cosmic Rayはより高度なカスタマイズが可能だが、セットアップの手間がかかる

注意点:

mutmutはDjangoやFastAPIなどのフレームワークコードに対しても使用できますが、DB接続やネットワークアクセスを伴うテストでは実行時間が大幅に増加します。--paths-to-mutate でビジネスロジック層に限定することを推奨します。

StrykerJS(TypeScript)でダッシュボード連携する

StrykerJSはTypeScript/JavaScript向けのミューテーションテストフレームワークです。HTMLレポートに加えて、Stryker Dashboardへの連携が特徴です。

# インストール(Vitest利用の場合)
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner

# 初期設定
npx stryker init

設定ファイル stryker.config.mjs でレポーター設定を行います。

// stryker.config.mjs
// Pythonの pytest.ini や setup.cfg に相当する設定ファイル
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
export default {
  testRunner: "vitest",
  reporters: [
    "html",         // ローカルHTMLレポート → reports/mutation/mutation.html
    "json",         // JSON出力(後述のメトリクス抽出で使用)
    "dashboard",    // Stryker Dashboard連携
    "progress",     // ターミナル進捗表示
  ],
  htmlReporter: {
    fileName: "reports/mutation/mutation.html",
  },
  jsonReporter: {
    fileName: "reports/mutation/mutation.json",
  },
  dashboard: {
    // 環境変数 STRYKER_DASHBOARD_API_KEY が必要
    reportType: "full",
  },
  // パフォーマンス最適化: 対象を限定
  mutate: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.d.ts"],
};
# ミューテーションテスト実行
npx stryker run

# 生成物:
# - reports/mutation/mutation.html  (ブラウザで開いて確認)
# - reports/mutation/mutation.json  (メトリクス抽出用)

Stryker Dashboardに連携すると、GitHub上のプロジェクトのミューテーションスコアをバッジとして表示できます。ダッシュボードはOSSプロジェクトなら無料で利用可能です。

なぜStrykerか:

  • mutation-testing-elementsというWeb Components ベースの統一可視化フレームワークを提供している
  • HTMLレポートでソースコード上にミュータントをインライン表示でき、Killed/Survivedのフィルタリングが可能
  • JSON出力を使えば、後述のカスタムダッシュボードへの連携も容易

PIT(Java)でマルチモジュール集約レポートを生成する

PIT(PITest)はJava/JVM向けのミューテーションテストツールで、MavenとGradleの両方に対応しています。特にマルチモジュールプロジェクトでのレポート集約機能が強力です。

Mavenプロジェクトの場合、pom.xml に以下を追加します。

<!-- pom.xml -->
<!-- Pythonの pyproject.toml に相当するビルド設定ファイル -->
<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.17.4</version>
    <configuration>
        <!-- HTMLとXML両方を出力(XML は集約レポートで必要) -->
        <outputFormats>
            <value>HTML</value>
            <value>XML</value>
        </outputFormats>
        <!-- カバレッジ情報もエクスポート(集約に必要) -->
        <exportLineCoverage>true</exportLineCoverage>
        <!-- 対象パッケージを限定してパフォーマンス改善 -->
        <targetClasses>
            <param>com.example.app.*</param>
        </targetClasses>
        <targetTests>
            <param>com.example.app.*Test</param>
        </targetTests>
    </configuration>
</plugin>
# 単一モジュールの実行
mvn org.pitest:pitest-maven:mutationCoverage

# マルチモジュール集約レポートの生成
mvn org.pitest:pitest-maven:mutationCoverage \
    org.pitest:pitest-maven:report-aggregate

PITのHTMLレポートでは、Line Coverage(行カバレッジ)とMutation Coverage(ミューテーションカバレッジ)が色分けで表示されます。

意味
薄い緑 Line Coverageあり
濃い緑 Mutation Coverageあり(ミュータントがKilled)
薄いピンク Line Coverageなし
濃いピンク Mutation Coverageなし(ミュータントがSurvived)

Gradleプロジェクトでのマルチモジュール集約は、gradle-pitest-pluginを使用します。

// build.gradle(ルートプロジェクト)
// Pythonの pyproject.toml + マルチパッケージ構成に相当
plugins {
    id 'info.solidsoft.pitest.aggregator' version '1.15.0'
}

subprojects {
    apply plugin: 'info.solidsoft.pitest'
    
    pitest {
        testPlugin = 'junit5'
        outputFormats = ['XML']  // 集約にはXML必須
        exportLineCoverage = true
        timestampedReports = false  // 集約時はタイムスタンプなし推奨
    }
}
# Gradleでの実行と集約
./gradlew pitest pitestReportAggregate
# → build/reports/pitest/ に集約レポートが出力

なぜPITか:

  • Java/JVMエコシステムで事実上の標準ツール(GitHub Stars 1,700+
  • マルチモジュール集約レポートにネイティブ対応しており、大規模プロジェクトでの運用に適している
  • Stryker Dashboardへのレポートアップロードにも対応(言語横断の可視化が可能)

制約条件:

PITはクラス単位でミュータントを生成するため、Kotlinのデータクラスやsealed classとの相性が良くない場合があります。Kotlin中心のプロジェクトではKureなどのKotlin専用ツールの検討も必要です。

ミューテーションスコアを時系列で記録・可視化する

ツール標準のHTMLレポートは「今のスナップショット」を見るには十分ですが、テスト品質が改善しているのか、劣化しているのかを判断するには時系列データが必要です。ここでは、CIパイプラインでメトリクスを抽出し、Prometheusに記録してGrafanaで可視化する方法を解説します。

CIでメトリクスを抽出するスクリプトを実装する

まず、各ツールの出力からメトリクスを抽出するPythonスクリプトを作成します。

# scripts/extract_mutation_metrics.py
"""ミューテーションテスト結果からメトリクスを抽出するスクリプト"""
import json
import sys
from pathlib import Path
from dataclasses import dataclass


@dataclass
class MutationMetrics:
    """ミューテーションテストのメトリクス"""
    total: int
    killed: int
    survived: int
    no_coverage: int
    timeout: int
    runtime_errors: int
    compile_errors: int

    @property
    def detected(self) -> int:
        return self.killed + self.timeout

    @property
    def undetected(self) -> int:
        return self.survived + self.no_coverage

    @property
    def valid(self) -> int:
        return self.detected + self.undetected

    @property
    def mutation_score(self) -> float:
        if self.valid == 0:
            return 0.0
        return (self.detected / self.valid) * 100

    @property
    def coverage_based_score(self) -> float:
        covered = self.detected + self.survived
        if covered == 0:
            return 0.0
        return (self.detected / covered) * 100


def parse_stryker_json(path: Path) -> MutationMetrics:
    """StrykerのJSON出力を解析する"""
    data = json.loads(path.read_text())
    killed = survived = no_cov = timeout = runtime = compile_err = 0

    for file_data in data.get("files", {}).values():
        for mutant in file_data.get("mutants", []):
            status = mutant["status"]
            if status == "Killed":
                killed += 1
            elif status == "Survived":
                survived += 1
            elif status == "NoCoverage":
                no_cov += 1
            elif status == "Timeout":
                timeout += 1
            elif status == "RuntimeError":
                runtime += 1
            elif status == "CompileError":
                compile_err += 1

    total = killed + survived + no_cov + timeout + runtime + compile_err
    return MutationMetrics(
        total=total,
        killed=killed,
        survived=survived,
        no_coverage=no_cov,
        timeout=timeout,
        runtime_errors=runtime,
        compile_errors=compile_err,
    )


def output_prometheus_format(metrics: MutationMetrics, project: str, module: str) -> str:
    """Prometheus Pushgateway用のメトリクス形式で出力する"""
    lines = [
        f'mutation_score{{project="{project}",module="{module}"}} {metrics.mutation_score:.2f}',
        f'mutation_coverage_based_score{{project="{project}",module="{module}"}} {metrics.coverage_based_score:.2f}',
        f'mutation_killed{{project="{project}",module="{module}"}} {metrics.killed}',
        f'mutation_survived{{project="{project}",module="{module}"}} {metrics.survived}',
        f'mutation_no_coverage{{project="{project}",module="{module}"}} {metrics.no_coverage}',
        f'mutation_timeout{{project="{project}",module="{module}"}} {metrics.timeout}',
        f'mutation_total_valid{{project="{project}",module="{module}"}} {metrics.valid}',
    ]
    return "\n".join(lines)


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: python extract_mutation_metrics.py <json_path> <project> <module>")
        sys.exit(1)
    
    json_path = Path(sys.argv[1])
    project = sys.argv[2]
    module = sys.argv[3]
    
    metrics = parse_stryker_json(json_path)
    print(f"Mutation Score: {metrics.mutation_score:.1f}%")
    print(f"Coverage-Based Score: {metrics.coverage_based_score:.1f}%")
    print(f"Killed: {metrics.killed}, Survived: {metrics.survived}, No Coverage: {metrics.no_coverage}")
    print()
    print("--- Prometheus Format ---")
    print(output_prometheus_format(metrics, project, module))

GitHub Actionsでメトリクスを記録するワークフローを構築する

以下のワークフローは、PRごとにミューテーションテストを実行し、メトリクスをアーティファクトとして保存する例です。

# .github/workflows/mutation-test.yml
name: Mutation Test & Metrics

on:
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 3 * * 1'  # 毎週月曜3:00 UTCに定期実行

jobs:
  mutation-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run Stryker
        run: npx stryker run
      
      - name: Extract metrics
        run: |
          python scripts/extract_mutation_metrics.py \
            reports/mutation/mutation.json \
            my-project \
            core \
            > metrics_output.txt
          cat metrics_output.txt
      
      - name: Upload mutation report
        uses: actions/upload-artifact@v4
        with:
          name: mutation-report-${{ github.sha }}
          path: |
            reports/mutation/mutation.html
            reports/mutation/mutation.json
            metrics_output.txt
      
      # Prometheus Pushgatewayへの送信(オプション)
      - name: Push metrics to Prometheus
        if: github.event_name == 'schedule'
        run: |
          # Prometheusメトリクスの抽出と送信
          grep -E '^mutation_' metrics_output.txt | \
            curl --data-binary @- \
            "${{ secrets.PUSHGATEWAY_URL }}/metrics/job/mutation-test/instance/my-project"

なぜ定期実行とPR実行を分けるか:

  • PR実行: 差分のみのミューテーションテストで高速フィードバック
  • 定期実行(週次): プロジェクト全体のメトリクスを記録してトレンド分析に使用

ハマりポイント:

Prometheus Pushgatewayへの送信では、ジョブ名とインスタンス名の組み合わせが一意でないとメトリクスが上書きされます。モジュール名をラベルに含めることで、マルチモジュールプロジェクトでも正しくメトリクスを区別できます。

Grafanaダッシュボードでトレンドを可視化する

Prometheusに記録されたメトリクスをGrafanaで可視化します。以下はダッシュボードのJSON定義の主要部分です。

{
  "dashboard": {
    "title": "Mutation Testing Quality Trends",
    "panels": [
      {
        "title": "Mutation Score Trend",
        "type": "timeseries",
        "targets": [
          {
            "expr": "mutation_score{project=\"my-project\"}",
            "legendFormat": "{{module}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "percent",
            "thresholds": {
              "steps": [
                { "color": "red", "value": 0 },
                { "color": "orange", "value": 60 },
                { "color": "green", "value": 80 }
              ]
            }
          }
        }
      },
      {
        "title": "Survived Mutants by Module",
        "type": "barchart",
        "targets": [
          {
            "expr": "mutation_survived{project=\"my-project\"}",
            "legendFormat": "{{module}}"
          }
        ]
      },
      {
        "title": "Score vs Coverage Gap",
        "type": "timeseries",
        "targets": [
          {
            "expr": "mutation_coverage_based_score{project=\"my-project\"} - mutation_score{project=\"my-project\"}",
            "legendFormat": "Gap ({{module}})"
          }
        ]
      }
    ]
  }
}

ダッシュボードに含めるべき3つのパネルは以下の通りです。

パネル 目的 閾値の目安
Mutation Score Trend スコアの時系列推移 赤: 0-60%、黄: 60-80%、緑: 80%+
Survived Mutants by Module モジュール別の生存ミュータント数 多いモジュールから改善優先
Score vs Coverage Gap Mutation ScoreとCoverage-Based Scoreの乖離 Gap > 20%でカバレッジ不足アラート

制約条件:

この構成はPrometheus + Grafanaのインフラが前提です。小規模チームでは、GitHub Actions のアーティファクトにJSON を蓄積し、定期的にPythonスクリプトで集計する軽量な代替手段もあります。Grafanaのセットアップコストが見合わない場合は、次のセクションで紹介する軽量アプローチを検討してください。

チーム横断でテスト品質を比較・改善する

ミューテーションスコアの可視化が単一プロジェクトで動き始めたら、次はチーム・モジュール横断の品質比較と改善優先度の判断に活用します。

モジュール別スコアヒートマップで改善優先度を判断する

複数モジュールのミューテーションスコアをヒートマップ形式で一覧表示すると、テスト改善の優先度が一目で判断できます。

# scripts/generate_heatmap.py
"""モジュール別ミューテーションスコアのヒートマップを生成する"""
import json
from pathlib import Path


def generate_markdown_heatmap(modules: dict[str, float]) -> str:
    """モジュール別スコアをMarkdownテーブルで出力する"""
    
    def score_emoji(score: float) -> str:
        if score >= 80:
            return "🟢"
        elif score >= 60:
            return "🟡"
        else:
            return "🔴"
    
    lines = ["| Module | Score | Status | Action |"]
    lines.append("|--------|-------|--------|--------|")
    
    # スコアの低い順にソート(改善優先度が高い順)
    for module, score in sorted(modules.items(), key=lambda x: x[1]):
        emoji = score_emoji(score)
        if score < 60:
            action = "テスト追加を優先"
        elif score < 80:
            action = "Survivedミュータント確認"
        else:
            action = "維持"
        lines.append(f"| {module} | {score:.1f}% | {emoji} | {action} |")
    
    return "\n".join(lines)


# 使用例
modules = {
    "auth": 85.2,
    "payment": 72.1,
    "notification": 45.3,
    "user-profile": 91.0,
    "data-pipeline": 58.7,
}

print(generate_markdown_heatmap(modules))

出力例:

Module Score Status Action
notification 45.3% 🔴 テスト追加を優先
data-pipeline 58.7% 🔴 テスト追加を優先
payment 72.1% 🟡 Survivedミュータント確認
auth 85.2% 🟢 維持
user-profile 91.0% 🟢 維持

この表から、notificationモジュール(45.3%)とdata-pipelineモジュール(58.7%)のテスト改善が最優先であることが即座にわかります。

Survivedミュータントの分析で具体的な改善アクションを導く

スコアが低いモジュールを特定したら、次はSurvivedミュータントの内訳を分析して具体的な改善アクションにつなげます。

# scripts/analyze_survived.py
"""Survivedミュータントの分析レポートを生成する"""
import json
from collections import Counter
from pathlib import Path


def analyze_survived_mutants(json_path: Path) -> dict:
    """Survivedミュータントをミューテーター種別ごとに集計する"""
    data = json.loads(json_path.read_text())
    survived_by_type: Counter = Counter()
    survived_examples: dict[str, list] = {}

    for filename, file_data in data.get("files", {}).items():
        for mutant in file_data.get("mutants", []):
            if mutant["status"] == "Survived":
                mutator = mutant.get("mutatorName", "Unknown")
                survived_by_type[mutator] += 1
                if mutator not in survived_examples:
                    survived_examples[mutator] = []
                if len(survived_examples[mutator]) < 3:
                    survived_examples[mutator].append({
                        "file": filename,
                        "location": mutant.get("location", {}),
                        "replacement": mutant.get("replacement", ""),
                    })

    return {
        "by_type": survived_by_type.most_common(),
        "examples": survived_examples,
    }


def format_analysis(analysis: dict) -> str:
    """分析結果をMarkdownで整形する"""
    lines = ["## Survivedミュータント分析\n"]
    lines.append("| ミューテーター種別 | 件数 | 推奨アクション |")
    lines.append("|-------------------|------|---------------|")

    action_map = {
        "ConditionalExpression": "境界値テストの追加",
        "EqualityOperator": "等価条件のエッジケース追加",
        "ArithmeticOperator": "計算結果の具体値アサーション追加",
        "StringLiteral": "文字列出力の検証追加",
        "BooleanLiteral": "True/False両方のパステスト",
        "ArrayDeclaration": "空配列・多要素のテスト追加",
        "BlockStatement": "ブロック削除時の副作用テスト",
    }

    for mutator, count in analysis["by_type"]:
        action = action_map.get(mutator, "該当箇所のアサーション強化")
        lines.append(f"| {mutator} | {count} | {action} |")

    return "\n".join(lines)

このスクリプトにより、「どの種類のミューテーションが生き残っているか」を定量的に把握できます。たとえば ConditionalExpression のSurvivedが多い場合、条件分岐の境界値テストが不足していることを示唆します。

品質トレンドからテスト劣化を早期検知する

ミューテーションスコアの時系列データが蓄積されると、品質劣化の兆候を検知できるようになります。以下のルールをGrafanaアラートに設定することを推奨します。

アラート条件 閾値 対応アクション
スコア急落 前週比 -5ポイント以上 直近のPRでテストなしコード追加がないか確認
継続的低下 3週連続で低下 テスト改善スプリントの計画
モジュール間格差 最高-最低 > 30ポイント 低スコアモジュールへのペアテスティング
カバレッジギャップ Coverage-Based - Mutation > 20ポイント カバレッジのない領域のテスト追加
# scripts/detect_degradation.py
"""品質劣化検知スクリプト(週次レポート用)"""
import json
from dataclasses import dataclass


@dataclass
class WeeklyScore:
    week: str
    module: str
    score: float


def detect_degradation(scores: list[WeeklyScore]) -> list[str]:
    """品質劣化パターンを検出してアラートメッセージを生成する"""
    alerts = []
    
    # モジュールごとに直近4週間のスコアを取得
    by_module: dict[str, list[WeeklyScore]] = {}
    for s in scores:
        by_module.setdefault(s.module, []).append(s)
    
    for module, weekly in by_module.items():
        weekly.sort(key=lambda x: x.week, reverse=True)
        
        if len(weekly) >= 2:
            diff = weekly[0].score - weekly[1].score
            if diff <= -5.0:
                alerts.append(
                    f"[ALERT] {module}: スコアが{abs(diff):.1f}pt急落 "
                    f"({weekly[1].score:.1f}% → {weekly[0].score:.1f}%)"
                )
        
        if len(weekly) >= 3:
            if all(weekly[i].score < weekly[i + 1].score for i in range(2)):
                alerts.append(
                    f"[WARNING] {module}: 3週連続でスコアが低下中 "
                    f"({weekly[2].score:.1f}% → {weekly[0].score:.1f}%)"
                )
    
    # モジュール間格差チェック
    latest_scores = {m: ws[0].score for m, ws in by_module.items() if ws}
    if latest_scores:
        max_score = max(latest_scores.values())
        min_score = min(latest_scores.values())
        if max_score - min_score > 30:
            min_module = min(latest_scores, key=latest_scores.get)
            alerts.append(
                f"[WARNING] モジュール間格差: {max_score:.1f}% - {min_score:.1f}% = "
                f"{max_score - min_score:.1f}pt({min_module}の改善を推奨)"
            )
    
    return alerts

よくある間違い: ミューテーションスコアの絶対値だけで品質を判断してしまうことです。スコア80%のモジュールでも、前週から10ポイント低下していれば問題があります。トレンド(変化の方向性)と絶対値の両方を見ることが重要です。

Prometheus + Grafanaを使わない軽量な可視化アプローチを検討する

Prometheus + Grafanaの構成はインフラの準備が必要です。小規模チームや個人プロジェクトでは、以下の軽量アプローチも有効です。

GitHub Actions アーティファクトとPythonによる集計

# scripts/lightweight_report.py
"""GitHub Actionsアーティファクトから軽量レポートを生成する"""
import json
import csv
from pathlib import Path
from datetime import datetime


def append_to_csv(metrics_json: Path, csv_path: Path, project: str, module: str):
    """メトリクスをCSVに追記する(時系列データの蓄積)"""
    data = json.loads(metrics_json.read_text())
    
    # Stryker JSONからメトリクスを抽出
    killed = survived = no_cov = timeout = 0
    for file_data in data.get("files", {}).values():
        for mutant in file_data.get("mutants", []):
            status = mutant["status"]
            if status == "Killed":
                killed += 1
            elif status == "Survived":
                survived += 1
            elif status == "NoCoverage":
                no_cov += 1
            elif status == "Timeout":
                timeout += 1
    
    detected = killed + timeout
    valid = detected + survived + no_cov
    score = (detected / valid * 100) if valid > 0 else 0.0
    
    row = {
        "date": datetime.now().strftime("%Y-%m-%d"),
        "project": project,
        "module": module,
        "mutation_score": f"{score:.2f}",
        "killed": killed,
        "survived": survived,
        "no_coverage": no_cov,
        "timeout": timeout,
        "total_valid": valid,
    }
    
    file_exists = csv_path.exists()
    with open(csv_path, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=row.keys())
        if not file_exists:
            writer.writeheader()
        writer.writerow(row)
    
    print(f"Appended: {project}/{module} = {score:.1f}% ({detected}/{valid})")
# .github/workflows/mutation-lightweight.yml
name: Mutation Test (Lightweight)

on:
  schedule:
    - cron: '0 3 * * 1'  # 週次

jobs:
  mutation-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run mutation test
        run: npx stryker run
      
      - name: Append metrics to CSV
        run: |
          python scripts/lightweight_report.py \
            reports/mutation/mutation.json \
            mutation_history.csv \
            my-project \
            core
      
      - name: Commit CSV update
        run: |
          git config user.name "github-actions"
          git config user.email "github-actions@github.com"
          git add mutation_history.csv
          git commit -m "chore: update mutation metrics" || echo "No changes"
          git push

この方法では、CSVファイルをGitリポジトリに蓄積し、必要に応じてスプレッドシートやmatplotlibで可視化できます。Grafanaの導入コストなしで始められるため、まずはこの軽量アプローチから試してみることを推奨します。

トレードオフ:

  • メリット: 追加インフラ不要、GitリポジトリだけでOK
  • デメリット: リアルタイムアラートなし、インタラクティブなダッシュボードなし

よくある問題と解決方法

問題 原因 解決方法
ミューテーションテストの実行時間が長すぎる プロジェクト全体に対して実行している --in-diff(cargo-mutants)やPR差分限定で実行。Strykerはmutate設定で対象を限定
等価ミュータントが多くスコアが不正確 コンパイラ最適化で同等のコードが生成される MetaのLLMベース検出(precision 0.79)が参考に。手動判定を最小化するためIgnored設定を活用
モジュール間でスコアが大きく異なる テスト文化・リソース配分の偏り ヒートマップで可視化し、低スコアモジュールに集中投資。ペアテスティングの導入
Grafanaの導入コストが高い Prometheus + Grafanaのインフラ構築が必要 CSVベースの軽量アプローチから段階的に移行。GitHub Pages + Chart.jsも代替案
CI実行コストの増加 毎PRで全ファイルのミューテーションテスト 週次の全体スキャン + PR時は差分のみの2層運用

まとめと次のステップ

まとめ:

  • ミューテーションテストは8つのミュータント状態と Mutation Score = Detected / Valid × 100 の計算式で、テストのバグ検出能力を定量化する
  • Stryker(TypeScript)、PIT(Java)、mutmut(Python)の各ツールがHTMLレポートとJSON/XML出力を提供し、可視化の基盤となる
  • CIパイプラインでメトリクスを時系列記録し、Grafanaダッシュボードでトレンド分析することで、テスト品質の劣化を早期検知できる
  • モジュール別ヒートマップとSurvivedミュータント分析により、テスト改善の優先度と具体的アクションを導出できる
  • 小規模チームではCSVベースの軽量アプローチから始め、段階的にPrometheus + Grafanaへ移行するのが現実的

次にやるべきこと:

  • 自分のプロジェクトで1つのモジュールを選び、ミューテーションテストを実行してベースラインスコアを計測する
  • CIに週次の定期実行を設定し、メトリクスの蓄積を開始する
  • 3週間分のデータが蓄積されたら、トレンドグラフを確認して改善計画を立てる

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?