T-CREATOR

Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化

Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化

Vitest でテストを実行していると、時々テストが失敗したり成功したりする「フレーキーテスト」に悩まされることはありませんか。CI/CD パイプラインで突然失敗するテストは、開発チームの生産性を大きく下げてしまいますね。本記事では、Vitest が提供するフレーク検知技術を深掘りし、--retry オプション、シード固定、ランダム順序実行の 3 つの技術を組み合わせて、テストの堅牢性を向上させる方法をご紹介します。

これらの機能を適切に運用することで、フレーキーテストを早期発見し、テストスイート全体の信頼性を高めることができますよ。

背景

Vitest とフレーキーテスト

Vitest は、Vite ベースの高速テストランナーとして、モダンな JavaScript/TypeScript プロジェクトで広く採用されています。しかし、どれほど優れたテストランナーでも、テストコード自体の品質や実行環境の影響で「フレーキーテスト」が発生することがあります。

フレーキーテストとは、コードに変更がないにもかかわらず、テスト実行のたびに結果が異なるテストのことです。フレーキーテストは以下のような原因で発生します。

#原因カテゴリ具体例
1タイミング依存非同期処理の待機不足、アニメーション完了待ち
2外部依存API 呼び出し、データベース状態、ネットワーク遅延
3テスト間の状態共有グローバル変数、モジュールキャッシュ、DOM 残存
4ランダム性乱数、日付・時刻、シャッフル処理
5実行順序依存テストファイル間の依存、セットアップ順序

Vitest のフレーク検知アプローチ

Vitest は、フレーキーテストの検知と対策のために、複数の機能を提供しています。これらの機能を組み合わせることで、フレーキーテストを早期に発見し、原因を特定しやすくなります。

mermaidflowchart TB
  dev["開発者"] -->|テスト実行| vitest["Vitest ランナー"]
  vitest -->|retry 機能| retry["失敗時自動再試行"]
  vitest -->|seed 固定| seed["乱数シード固定"]
  vitest -->|順序制御| order["実行順序制御"]

  retry -->|成功| pass["テスト成功<br/>(フレーク疑い)"]
  retry -->|失敗| fail["テスト失敗<br/>(真の不具合)"]

  seed -->|再現性確保| repro["フレーク再現"]
  order -->|依存検知| detect["順序依存検知"]

  pass --> report["レポート出力"]
  fail --> report
  repro --> report
  detect --> report

上図は、Vitest のフレーク検知機能の全体像を示しています。各機能が連携することで、フレーキーテストの検知精度が高まりますね。

課題

フレーキーテストがもたらす問題

フレーキーテストは、開発プロセスに深刻な影響を及ぼします。以下のような課題が発生することが多いでしょう。

生産性の低下

CI/CD パイプラインでテストが失敗すると、開発者はその原因を調査する必要があります。しかし、フレーキーテストの場合、再実行すると成功してしまうため、真の不具合なのか、フレークなのかを判断するのに時間がかかります。

信頼性の低下

フレーキーテストが頻発すると、テストスイート全体への信頼が失われてしまいます。「またフレークか」と判断して本当の不具合を見逃すリスクが高まりますね。

デバッグの困難さ

フレーキーテストは再現が難しいため、原因の特定が困難です。特に、実行順序やタイミングに依存する問題は、ローカル環境では再現せず、CI 環境でのみ発生することもあります。

従来のアプローチの限界

フレーキーテスト対策として、従来は以下のような手動アプローチが取られてきました。

#アプローチ限界
1手動で複数回実行時間がかかり、統計的な信頼性が低い
2sleep を追加テスト実行時間が長くなり、根本解決にならない
3テスト順序を手動調整スケールせず、新しいテスト追加時に破綻
4フレーキーテストを無視問題を先送りし、技術的負債が蓄積

これらの手動アプローチでは、フレーキーテストの根本原因を特定できず、長期的な解決策にはなりません。

解決策

Vitest は、フレーキーテストに対処するための 3 つの強力な機能を提供しています。これらを適切に組み合わせることで、フレーク検知の精度を高め、原因特定を効率化できます。

解決策 1: --retry オプションによる自動再試行

--retry オプションは、失敗したテストを自動的に再実行する機能です。フレーキーテストの場合、再実行で成功することが多いため、真の不具合とフレークを区別できます。

mermaidstateDiagram-v2
  [*] --> TestRun: テスト実行開始
  TestRun --> Success: 1回目成功
  TestRun --> Failed1: 1回目失敗

  Failed1 --> Retry1: retry (1/3)
  Retry1 --> Success: 2回目成功
  Retry1 --> Failed2: 2回目失敗

  Failed2 --> Retry2: retry (2/3)
  Retry2 --> Success: 3回目成功
  Retry2 --> Failed3: 3回目失敗

  Failed3 --> Retry3: retry (3/3)
  Retry3 --> Success: 4回目成功
  Retry3 --> FinalFail: 4回目失敗

  Success --> [*]: フレーク疑い報告
  FinalFail --> [*]: 真の不具合報告

上図は、--retry の動作フローを示しています。最大再試行回数まで自動で再実行し、成功した場合はフレークの可能性を報告します。

設定方法

コマンドラインから直接指定する方法と、設定ファイルで指定する方法があります。

typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // 失敗したテストを最大3回まで再試行
    retry: 3,
  },
});

このように設定ファイルで指定すると、すべてのテスト実行で自動的に適用されます。プロジェクト全体で統一したポリシーを適用したい場合に便利ですね。

コマンドラインから実行する場合は、以下のように指定します。

bash# 失敗したテストを3回まで再試行
yarn vitest --retry=3

CI/CD での活用

CI/CD パイプラインでは、より厳格な設定が推奨されます。例えば、retry 回数を増やしてフレークを徹底的に検知する方法があります。

yaml# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      # 依存関係をインストール
      - run: yarn install

      # フレーク検知モードでテスト実行
      - run: yarn vitest --retry=5 --reporter=verbose
        env:
          CI: true

CI 環境では、retry 回数を多めに設定することで、フレーキーテストを確実に検知できます。

解決策 2: シード固定による再現性の確保

Vitest は、テストで使用される乱数のシード値を固定する機能を提供しています。これにより、ランダム性に依存するテストの動作を再現可能にできます。

シードの仕組み

Vitest は内部的に擬似乱数生成器を使用しており、シード値を固定することで、同じ順序で同じ乱数列を生成できます。

typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // シード値を固定して再現性を確保
    seed: 12345,
  },
});

シード値を固定すると、テストで使用されるすべての乱数が同じ順序で生成されます。フレーキーテストが乱数に依存している場合、原因の特定が容易になりますね。

コマンドラインでの指定

特定のシード値でテストを実行したい場合は、コマンドラインから指定できます。

bash# 特定のシード値でテスト実行
yarn vitest --seed=12345

# ランダムなシード値を生成して実行
yarn vitest --seed=$RANDOM

環境変数 $RANDOM を使用すると、実行ごとに異なるシード値でテストできます。これにより、ランダム性に依存するフレークを検出しやすくなります。

シード値の記録

Vitest は、テスト実行時に使用したシード値をレポートに出力します。フレークが発生した場合、そのシード値を使って再現できます。

typescript// テスト実行ログの例
// Vitest v1.0.0
// Seed: 12345
//
// Test Files  1 passed (1)
//      Tests  10 passed (10)

このログからシード値を取得し、再実行することで、フレークの再現性を確保できますね。

解決策 3: ランダム順序実行による順序依存の検知

テスト実行順序に依存するフレークは、特に検知が難しい問題です。Vitest は、テストをランダムな順序で実行する機能を提供しており、順序依存を早期に発見できます。

mermaidflowchart LR
  tests["テストスイート"] -->|通常実行| seq["順次実行<br/>(A→B→C)"]
  tests -->|shuffle| shuffle["ランダム順序<br/>(C→A→B)"]

  seq -->|成功| ok1["見かけ上OK"]
  shuffle -->|失敗| fail["順序依存を検知"]
  shuffle -->|成功| ok2["真の独立性"]

  ok1 -->|隠れたリスク| risk["順序依存の見逃し"]
  fail -->|改善| fix["テスト修正"]
  ok2 -->|安全| safe["堅牢なテスト"]

上図は、ランダム順序実行により、通常実行では検知できない順序依存を発見できることを示しています。

設定方法

typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // テストをランダムな順序で実行
    sequence: {
      shuffle: true,
    },
    // シード値も併用して再現性を確保
    seed: 12345,
  },
});

shuffle: true を設定すると、テストファイル内のテストケース、およびテストファイル間の実行順序がランダム化されます。

コマンドラインでの実行

bash# ランダム順序でテスト実行
yarn vitest --sequence.shuffle=true

# シード値も指定して再現可能に
yarn vitest --sequence.shuffle=true --seed=12345

CI/CD パイプラインでは、毎回異なるシード値でランダム実行することで、順序依存を継続的に監視できます。

具体例

ここでは、実際のプロジェクトでフレーク検知技術を運用する具体例をご紹介します。

具体例 1: API テストのフレーク検知

API テストは、ネットワーク遅延やタイムアウトの影響でフレークが発生しやすい領域です。以下は、フレーク検知機能を活用した例です。

テストコード(フレークを含む例)

typescript// src/api/user.test.ts
import { describe, it, expect } from 'vitest';
import { fetchUser } from './user';

describe('User API', () => {
  it('should fetch user data', async () => {
    // この テストはネットワーク遅延でフレークする可能性がある
    const user = await fetchUser(1);

    expect(user).toBeDefined();
    expect(user.id).toBe(1);
    expect(user.name).toBe('John Doe');
  });
});

このテストは、外部 API に依存しており、ネットワークの状態によって失敗することがあります。

Vitest 設定(フレーク検知モード)

typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // フレーク検知のための設定
    retry: 3,
    seed: 42,
    sequence: {
      shuffle: true,
    },
    // タイムアウトも調整
    testTimeout: 10000,
  },
});

この設定により、失敗したテストは最大 3 回まで再試行され、フレークかどうかを判定できます。

実行結果の分析

bash# フレーク検知モードで実行
yarn vitest --retry=3 --reporter=verbose

実行結果として、以下のようなレポートが出力されます。

typescript// RERUN  src/api/user.test.ts > User API > should fetch user data
// RERUN  src/api/user.test.ts > User API > should fetch user data
// PASS   src/api/user.test.ts > User API > should fetch user data (3rd attempt)
//
// Test Files  1 passed (1)
//      Tests  1 passed (1)
// Flaky Tests  1 test passed after retries

このレポートから、テストが 3 回目の試行で成功したことがわかり、フレークの可能性が高いと判断できます。

改善策の実装

フレークが検知されたら、根本原因を解決するためにテストを改善します。

typescript// src/api/user.test.ts
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './user';

// モック化してネットワーク依存を排除
vi.mock('./user', () => ({
  fetchUser: vi.fn(),
}));

describe('User API', () => {
  it('should fetch user data', async () => {
    // モックを使用して安定したテストに改善
    const mockUser = { id: 1, name: 'John Doe' };
    vi.mocked(fetchUser).mockResolvedValue(mockUser);

    const user = await fetchUser(1);

    expect(user).toBeDefined();
    expect(user.id).toBe(1);
    expect(user.name).toBe('John Doe');
    expect(fetchUser).toHaveBeenCalledWith(1);
  });
});

モック化により、外部依存を排除し、フレークを根本的に解決できました。

具体例 2: DOM 操作テストの順序依存検知

フロントエンドテストでは、DOM の状態が前のテストに影響されることで、順序依存のフレークが発生することがあります。

テストコード(順序依存を含む例)

typescript// src/components/Counter.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter Component', () => {
  it('should increment counter', async () => {
    const wrapper = mount(Counter);
    const button = wrapper.find('button');

    await button.trigger('click');

    // 初期値が 0 と仮定しているが、前のテストの影響を受ける可能性
    expect(wrapper.text()).toContain('Count: 1');
  });

  it('should decrement counter', async () => {
    const wrapper = mount(Counter);
    const decrementBtn = wrapper.findAll('button')[1];

    await decrementBtn.trigger('click');

    // この テストも初期状態を仮定
    expect(wrapper.text()).toContain('Count: -1');
  });
});

これらのテストは、個別には成功しますが、実行順序によっては失敗する可能性があります。

ランダム順序実行で検知

bash# ランダム順序で実行してフレークを検知
yarn vitest --sequence.shuffle=true --seed=$RANDOM

ランダム順序で実行すると、順序依存のフレークが検出されます。

typescript// FAIL  src/components/Counter.test.ts > Counter Component > should decrement counter
// AssertionError: expected 'Count: 0' to contain 'Count: -1'
//
// Seed: 98765

シード値 98765 で失敗したことが記録され、同じシード値で再現できます。

改善策の実装

各テストで独立した状態を確保するように修正します。

typescript// src/components/Counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { mount, VueWrapper } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter Component', () => {
  let wrapper: VueWrapper;

  // 各テスト前に新しいインスタンスを作成
  beforeEach(() => {
    wrapper = mount(Counter);
  });

  it('should increment counter', async () => {
    const button = wrapper.find('button');

    await button.trigger('click');

    // 毎回初期状態から開始されるため安定
    expect(wrapper.text()).toContain('Count: 1');
  });

  it('should decrement counter', async () => {
    const decrementBtn = wrapper.findAll('button')[1];

    await decrementBtn.trigger('click');

    // こちらも独立して動作
    expect(wrapper.text()).toContain('Count: -1');
  });
});

beforeEach フックで毎回新しいコンポーネントインスタンスを作成することで、順序依存を解消できました。

具体例 3: CI/CD パイプラインでの統合運用

最後に、これらの技術を CI/CD パイプラインで統合運用する実践例をご紹介します。

GitHub Actions での設定

yaml# .github/workflows/vitest-flake-detection.yml
name: Vitest Flake Detection

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    # 毎日深夜にフレーク検知を実行
    - cron: '0 0 * * *'

jobs:
  test:
    name: Test with Flake Detection
    runs-on: ubuntu-latest

    strategy:
      matrix:
        # 複数回実行してフレークを検知
        run: [1, 2, 3]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'

      - name: Install Dependencies
        run: yarn install --frozen-lockfile

ここまでで、基本的なセットアップが完了しました。次に、フレーク検知の実行ステップを追加します。

yaml# フレーク検知モードでテスト実行
- name: Run Tests with Flake Detection
  run: |
    yarn vitest \
      --retry=3 \
      --sequence.shuffle=true \
      --seed=${{ github.run_id }}${{ matrix.run }} \
      --reporter=verbose \
      --reporter=json \
      --outputFile=test-results-${{ matrix.run }}.json
  env:
    CI: true

# テスト結果をアーティファクトとして保存
- name: Upload Test Results
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: test-results-${{ matrix.run }}
    path: test-results-${{ matrix.run }}.json

この設定により、同じコミットに対して 3 回テストを実行し、フレークを検知します。シード値には github.run_id を使用することで、再現可能性を確保しつつ、各実行で異なる順序をテストできますね。

フレーク分析ジョブの追加

yamlanalyze:
  name: Analyze Flake Detection Results
  runs-on: ubuntu-latest
  needs: test
  if: always()

  steps:
    - name: Download Test Results
      uses: actions/download-artifact@v4
      with:
        path: test-results

    - name: Analyze Flakes
      run: |
        echo "## Flake Detection Report" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY

        # 各実行結果を比較してフレークを検出
        node analyze-flakes.js test-results/

分析スクリプトで、複数回の実行結果を比較し、フレーキーテストを特定できます。

分析スクリプトの実装

typescript// analyze-flakes.js
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';

// テスト結果を読み込む関数
function loadTestResults(dir) {
  const files = readdirSync(dir, { recursive: true });
  const jsonFiles = files.filter((f) =>
    f.endsWith('.json')
  );

  return jsonFiles.map((file) => {
    const content = readFileSync(join(dir, file), 'utf-8');
    return JSON.parse(content);
  });
}

この関数で、すべてのテスト結果ファイルを読み込みます。

typescript// フレークを検出する関数
function detectFlakes(results) {
  const testResults = new Map();

  // 各実行結果からテストの成否を集計
  results.forEach((result, runIndex) => {
    result.testResults?.forEach((test) => {
      const key = `${test.name}`;

      if (!testResults.has(key)) {
        testResults.set(key, []);
      }

      testResults.get(key).push({
        run: runIndex + 1,
        status: test.status,
        duration: test.duration,
      });
    });
  });

  return testResults;
}

この関数で、同じテストの複数回実行結果を集計します。

typescript// フレークをレポートする関数
function reportFlakes(testResults) {
  const flakes = [];

  testResults.forEach((runs, testName) => {
    // 実行結果が異なる場合はフレークと判定
    const statuses = runs.map((r) => r.status);
    const hasFailure = statuses.includes('failed');
    const hasSuccess = statuses.includes('passed');

    if (hasFailure && hasSuccess) {
      flakes.push({
        name: testName,
        runs: runs,
      });
    }
  });

  return flakes;
}

成功と失敗の両方が含まれている場合、フレーキーテストと判定します。

typescript// メイン処理
function main() {
  const resultsDir = process.argv[2] || './test-results';

  console.log('Loading test results...');
  const results = loadTestResults(resultsDir);

  console.log('Detecting flakes...');
  const testResults = detectFlakes(results);
  const flakes = reportFlakes(testResults);

  if (flakes.length === 0) {
    console.log('✓ No flakes detected!');
    process.exit(0);
  }

  console.log(`⚠ Found ${flakes.length} flaky test(s):`);
  console.log('');

  flakes.forEach((flake) => {
    console.log(`- ${flake.name}`);
    flake.runs.forEach((run) => {
      console.log(
        `  Run ${run.run}: ${run.status} (${run.duration}ms)`
      );
    });
    console.log('');
  });

  process.exit(1);
}

main();

このスクリプトにより、フレーキーテストを自動的に検出し、CI/CD パイプラインで警告できます。

運用のポイント

フレーク検知を継続的に運用するためのポイントを表にまとめます。

#ポイント説明
1定期実行スケジュールトリガーで毎日実行し、継続的に監視
2複数回実行matrix 戦略で同じコミットを複数回テストしてフレークを検出
3シード値記録github.run_id を使用して再現可能性を確保
4結果の保存アーティファクトとして保存し、後から分析可能に
5レポート生成GITHUB_STEP_SUMMARY に結果を出力して可視化

これらのポイントを押さえることで、フレーク検知を効果的に運用できますね。

まとめ

本記事では、Vitest のフレーク検知技術として、--retry オプション、シード固定、ランダム順序実行の 3 つの機能を詳しく解説しました。

重要なポイント

  • --retry オプション: 失敗したテストを自動再試行し、フレークと真の不具合を区別できます
  • シード固定: 乱数のシード値を固定することで、ランダム性に依存するテストの再現性を確保できます
  • ランダム順序実行: テストをランダムな順序で実行することで、順序依存のフレークを早期発見できます

運用のベストプラクティス

これらの技術を効果的に運用するためには、以下のアプローチが推奨されます。

  1. ローカル開発: 通常のテスト実行では retry を無効化し、高速なフィードバックを優先する
  2. PR チェック: retry を有効化し、マージ前にフレークを検知する
  3. 定期実行: ランダム順序実行と複数回実行を組み合わせ、継続的にフレークを監視する
  4. フレーク検出時: シード値を記録し、再現可能な状態で原因を特定する

テストの信頼性向上

フレーキーテストは、開発チームの生産性と CI/CD パイプラインの信頼性を大きく損ないます。Vitest が提供するフレーク検知技術を活用することで、早期にフレークを発見し、堅牢なテストスイートを構築できますよ。

フレーク検知は一度設定すれば終わりではなく、継続的な監視と改善が重要です。本記事でご紹介した手法を参考に、プロジェクトに適したフレーク検知戦略を構築してくださいね。

関連リンク