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 | 手動で複数回実行 | 時間がかかり、統計的な信頼性が低い |
2 | sleep を追加 | テスト実行時間が長くなり、根本解決にならない |
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
オプション: 失敗したテストを自動再試行し、フレークと真の不具合を区別できます- シード固定: 乱数のシード値を固定することで、ランダム性に依存するテストの再現性を確保できます
- ランダム順序実行: テストをランダムな順序で実行することで、順序依存のフレークを早期発見できます
運用のベストプラクティス
これらの技術を効果的に運用するためには、以下のアプローチが推奨されます。
- ローカル開発: 通常のテスト実行では retry を無効化し、高速なフィードバックを優先する
- PR チェック: retry を有効化し、マージ前にフレークを検知する
- 定期実行: ランダム順序実行と複数回実行を組み合わせ、継続的にフレークを監視する
- フレーク検出時: シード値を記録し、再現可能な状態で原因を特定する
テストの信頼性向上
フレーキーテストは、開発チームの生産性と CI/CD パイプラインの信頼性を大きく損ないます。Vitest が提供するフレーク検知技術を活用することで、早期にフレークを発見し、堅牢なテストスイートを構築できますよ。
フレーク検知は一度設定すれば終わりではなく、継続的な監視と改善が重要です。本記事でご紹介した手法を参考に、プロジェクトに適したフレーク検知戦略を構築してくださいね。
関連リンク
- article
Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化
- article
Vitest テストアーキテクチャ技術:Unit / Integration / Contract の三層設計ガイド
- article
Vitest `vi` API 技術チートシート:`mock` / `fn` / `spyOn` / `advanceTimersByTime` 一覧
- article
Vitest × jsdom / happy-dom 技術セットアップ:最小構成と落とし穴
- article
5 分で導入!Vite × Vitest 型付きユニットテスト環境の最短手順
- article
【解決策】Vitest HMR 連携でテストが落ちる技術的原因と最短解決
- article
NestJS 監視運用:SLI/SLO とダッシュボード設計(Prometheus/Grafana/Loki)
- article
WebRTC AV1/VP9/H.264 ベンチ比較 2025:画質・CPU/GPU 負荷・互換性を実測
- article
MySQL アラート設計としきい値:レイテンシ・エラー率・レプリカ遅延の基準
- article
Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化
- article
Motion(旧 Framer Motion)デザインレビュー運用:Figma パラメータ同期と差分共有のワークフロー
- article
esbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来