Jest のフレークテスト撲滅作戦:重試行・乱数固定・リトライ設計の実務
テストを実行するたびに結果が変わる。こんな経験はありませんか?
特定の条件下でのみ失敗するテストは、開発チームにとって大きなストレスとなります。今日は成功したのに、明日は失敗する。このような「フレークテスト」は、CI/CD パイプラインの信頼性を損ない、開発速度を低下させる原因となるでしょう。
本記事では、Jest を使ったテスト環境において、フレークテストを撲滅するための実践的な手法を解説します。重試行メカニズムの実装から、乱数の固定、そしてリトライ設計のベストプラクティスまで、実務で即座に活用できるテクニックをお届けいたしますね。
背景
フレークテストとは
フレークテスト(Flaky Test)とは、コードに変更がないにもかかわらず、実行するたびに成功したり失敗したりする不安定なテストのことです。
この問題は、JavaScript や TypeScript を使った Web アプリケーション開発において特に顕著に現れます。非同期処理、ネットワークリクエスト、タイマー、乱数生成などが絡むテストでは、実行タイミングや環境の微妙な違いによって結果が変動しやすいのです。
フレークテストが発生する主な要因を図で整理してみましょう。以下の図は、フレークテストを引き起こす代表的な要因の関係性を示しています。
mermaidflowchart TB
test["テスト実行"]
async["非同期処理<br/>タイミング依存"]
random["乱数生成<br/>予測不可能"]
network["ネットワーク<br/>外部依存"]
timer["タイマー<br/>時間依存"]
state["共有状態<br/>テスト間干渉"]
test --> async
test --> random
test --> network
test --> timer
test --> state
async --> flaky["フレークテスト発生"]
random --> flaky
network --> flaky
timer --> flaky
state --> flaky
図の要点:
- テスト実行時に 5 つの主要な不安定要因が存在する
- それぞれが独立してフレークテストを引き起こす可能性がある
- 複数の要因が組み合わさるとさらに不安定になる
Jest におけるフレークテストの影響
Jest は JavaScript エコシステムで最も広く使われているテストフレームワークの一つです。しかし、その強力な機能を持ってしても、フレークテストの問題からは逃れられません。
実際の開発現場では、以下のような問題が発生しています。
| # | 問題 | 影響 |
|---|---|---|
| 1 | CI/CD パイプラインが不規則に失敗 | デプロイの遅延、開発速度の低下 |
| 2 | 開発者がテスト結果を信頼できない | テストの再実行を繰り返し、時間の浪費 |
| 3 | 本当のバグを見逃す可能性 | 品質低下、本番環境での障害 |
| 4 | チームの士気低下 | テスト文化の崩壊、技術的負債の増加 |
これらの問題を解決するためには、体系的なアプローチが必要です。
課題
フレークテストの典型的なパターン
実務でよく遭遇するフレークテストのパターンを理解することが、問題解決の第一歩となります。
パターン 1:非同期処理の待機不足
Promise や async/await を使った非同期処理で、適切な待機を行わないケースです。
typescript// 問題のあるテストコード例
test('ユーザーデータを取得する', () => {
const result = fetchUserData(); // Promise を返す関数
expect(result.name).toBe('太郎'); // エラー: result は Promise
});
このテストは、fetchUserData() が Promise を返すため、まだデータが取得されていない状態で検証を行ってしまいます。実行環境によっては偶然成功することもあり、フレークテストとなるのです。
パターン 2:乱数に依存したテスト
Math.random() などの乱数生成を使用している場合、実行のたびに異なる値が生成されます。
typescript// 乱数に依存する問題のあるコード
function generateId() {
return `user_${Math.random().toString(36).substr(2, 9)}`;
}
test('ID が正しく生成される', () => {
const id = generateId();
expect(id).toBe('user_abc123xyz'); // 実行ごとに異なる値
});
このようなテストは、期待値を固定できないため、本質的に不安定となります。
パターン 3:タイマーとタイミング依存
setTimeout や setInterval を使った処理では、実行環境の負荷によってタイミングがずれることがあります。
typescript// タイマーに依存する問題のあるコード
test('遅延処理が実行される', (done) => {
let executed = false;
setTimeout(() => {
executed = true;
}, 100);
setTimeout(() => {
expect(executed).toBe(true); // 環境によっては失敗
done();
}, 150);
});
システムの負荷が高い場合、100ms の遅延が実際には 200ms かかることもあり、テストが不安定になるのです。
以下の図は、フレークテストの発生から問題の連鎖までを示しています。
mermaidsequenceDiagram
participant Dev as 開発者
participant CI as CI/CD
participant Test as テスト実行
participant Result as 結果
Dev->>CI: コミット&プッシュ
CI->>Test: テスト実行開始
Test->>Test: 1回目: 成功
Test->>Result: ✓ 成功
Dev->>CI: 同じコードで再実行
CI->>Test: テスト実行開始
Test->>Test: 2回目: 失敗
Test->>Result: ✗ 失敗
Result->>Dev: 混乱と時間の浪費
Dev->>CI: テスト再実行
CI->>Test: 3回目実行
Test->>Test: 成功したり失敗したり
図の要点:
- 同じコードでも実行のたびに結果が変わる
- 開発者は問題の原因を特定できず時間を浪費
- CI/CD の信頼性が損なわれる
フレークテストがもたらす実害
フレークテストは単なる不便ではなく、以下のような深刻な実害をもたらします。
開発速度の低下が最も顕著な影響でしょう。開発者はテストが失敗するたびに、それが本当のバグなのか、フレークテストなのかを判断する必要があります。この判断には時間がかかり、本来の開発作業から注意が逸れてしまいますね。
さらに、CI/CD パイプラインでフレークテストが発生すると、デプロイプロセス全体が停止します。緊急のホットフィックスをデプロイしたい場合でも、フレークテストの失敗により数時間待たされることもあるのです。
最も危険なのは、テストへの信頼喪失です。テスト結果が信頼できなくなると、開発者は「また失敗したけど、どうせフレークテストだろう」と考え、本当のバグを見逃してしまう可能性があります。
解決策
アプローチ 1:Jest の重試行機能を活用する
Jest には、失敗したテストを自動的に再実行する機能があります。これを活用することで、一時的な不安定性によるテスト失敗を吸収できます。
jest-retries プラグインの導入
jest-retries は、個別のテストケースやテストスイートに対して重試行を設定できるプラグインです。
まず、パッケージをインストールします。
bashyarn add -D jest-retries
次に、Jest の設定ファイルでプラグインを有効化します。
javascript// jest.config.js
module.exports = {
// その他の設定...
setupFilesAfterEnv: [
'jest-retries/lib/index.js',
'<rootDir>/jest.setup.js',
],
};
この設定により、すべてのテストファイルで jest-retries の機能が利用可能になります。
テストレベルでの重試行設定
個別のテストケースに対して、重試行回数を指定できます。
typescript// src/__tests__/api.test.ts
describe('API テスト', () => {
// 特定のテストのみ3回まで重試行
test('ユーザー一覧を取得する', async () => {
jest.retryTimes(3);
const response = await fetch('/api/users');
const users = await response.json();
expect(users).toHaveLength(10);
});
});
この例では、テストが失敗した場合、最大 3 回まで自動的に再実行されます。3 回すべて失敗した場合のみ、テスト全体が失敗となるのです。
スイートレベルでの重試行設定
テストスイート全体に対して重試行を設定することもできます。
typescript// src/__tests__/integration.test.ts
describe('統合テスト', () => {
// このスイート内のすべてのテストで重試行を有効化
beforeAll(() => {
jest.retryTimes(2);
});
test('ログイン処理', async () => {
// 重試行が自動的に適用される
const result = await login(
'user@example.com',
'password'
);
expect(result.success).toBe(true);
});
test('データ更新処理', async () => {
// このテストにも重試行が適用される
const result = await updateProfile({ name: '太郎' });
expect(result.updated).toBe(true);
});
});
beforeAll フックで設定することで、スイート内のすべてのテストに重試行が適用されます。
アプローチ 2:乱数を固定する
乱数が原因でテストが不安定になる場合、シード値を固定することで再現性を確保できます。
Math.random のモック化
Jest のモック機能を使って、Math.random() を固定値に置き換えます。
typescript// src/__tests__/random.test.ts
describe('乱数を含む処理のテスト', () => {
beforeEach(() => {
// Math.random を固定値に置き換え
let count = 0;
jest
.spyOn(global.Math, 'random')
.mockImplementation(() => {
// 0.1, 0.2, 0.3... と順番に返す
count += 0.1;
return count % 1.0;
});
});
afterEach(() => {
// モックを元に戻す
jest.restoreAllMocks();
});
});
この設定により、テスト内での Math.random() の呼び出しは予測可能な値を返すようになります。
seedrandom ライブラリの活用
より高度な制御が必要な場合は、seedrandom ライブラリを使用します。
bashyarn add -D seedrandom
yarn add -D @types/seedrandom
シード値を指定して、再現可能な乱数列を生成します。
typescript// src/__tests__/seeded-random.test.ts
import seedrandom from 'seedrandom';
describe('シード固定による乱数テスト', () => {
test('同じシードからは同じ乱数列が生成される', () => {
// シード値 'test-seed' で初期化
const rng = seedrandom('test-seed');
// 最初の3つの乱数を取得
const values = [rng(), rng(), rng()];
// 期待値と一致することを検証
expect(values[0]).toBeCloseTo(0.730967787376213);
expect(values[1]).toBeCloseTo(0.24871906405195594);
expect(values[2]).toBeCloseTo(0.658614970743656);
});
});
同じシード値を使えば、常に同じ乱数列が生成されるため、テストの再現性が保証されます。
プロダクションコードへの依存注入
テスト時のみ乱数生成器を差し替える設計にすることで、プロダクションコードの変更を最小限に抑えられます。
typescript// src/utils/id-generator.ts
// 乱数生成器のインターフェース
interface RandomGenerator {
next(): number;
}
// デフォルトの乱数生成器
class DefaultRandomGenerator implements RandomGenerator {
next(): number {
return Math.random();
}
}
// ID 生成クラス
export class IdGenerator {
constructor(
private rng: RandomGenerator = new DefaultRandomGenerator()
) {}
generate(): string {
const randomPart = this.rng()
.next()
.toString(36)
.substr(2, 9);
return `user_${randomPart}`;
}
}
テスト時には、固定値を返す乱数生成器を注入します。
typescript// src/__tests__/id-generator.test.ts
import { IdGenerator } from '../utils/id-generator';
class FixedRandomGenerator {
next(): number {
return 0.123456789; // 常に同じ値を返す
}
}
test('ID が一貫して生成される', () => {
const generator = new IdGenerator(
new FixedRandomGenerator()
);
const id1 = generator.generate();
const id2 = generator.generate();
// 常に同じ ID が生成される
expect(id1).toBe('user_0dkif0yoe');
expect(id2).toBe('user_0dkif0yoe');
});
この設計により、テストの安定性とプロダクションコードの独立性が両立できます。
アプローチ 3:非同期処理の適切な待機
非同期処理が原因のフレークテストには、適切な待機メカニズムを実装することが重要です。
async/await の正しい使用
Promise を返す関数は、必ず await で待機します。
typescript// src/__tests__/async-correct.test.ts
describe('非同期処理の正しいテスト', () => {
test('ユーザーデータを取得する', async () => {
// async キーワードを使用し、await で待機
const user = await fetchUserData(123);
expect(user.id).toBe(123);
expect(user.name).toBe('太郎');
});
});
テスト関数を async として定義し、非同期処理を await で待機することで、処理が完了してから検証が行われます。
waitFor によるポーリング
条件が満たされるまで待機する場合は、waitFor を使用します。
typescript// src/__tests__/wait-for.test.ts
import { waitFor } from '@testing-library/react';
test('データが読み込まれるまで待機', async () => {
// データ取得を開始
const { getByText } = render(
<UserProfile userId={123} />
);
// 最大5秒間、条件が満たされるまで100msごとにチェック
await waitFor(
() => {
expect(getByText('太郎')).toBeInTheDocument();
},
{ timeout: 5000, interval: 100 }
);
});
waitFor は指定した条件が満たされるまでポーリングを続け、タイムアウトした場合のみ失敗となります。
タイマーのモック化
setTimeout や setInterval を使った処理では、Jest のタイマーモックを活用します。
typescript// src/__tests__/timer-mock.test.ts
describe('タイマーのモック', () => {
beforeEach(() => {
// タイマーをモック化
jest.useFakeTimers();
});
afterEach(() => {
// 実際のタイマーに戻す
jest.useRealTimers();
});
test('遅延実行が正しく動作する', () => {
const callback = jest.fn();
// 1000ms 後に実行される関数
setTimeout(callback, 1000);
// まだ実行されていない
expect(callback).not.toHaveBeenCalled();
// 時間を1000ms進める
jest.advanceTimersByTime(1000);
// コールバックが実行された
expect(callback).toHaveBeenCalledTimes(1);
});
});
jest.useFakeTimers() により、時間の経過を制御でき、実際に待機する必要がなくなります。
アプローチ 4:テスト間の独立性を確保する
テスト間で状態が共有されることで発生するフレークテストには、適切なセットアップとクリーンアップが必要です。
beforeEach / afterEach の活用
各テストの前後で状態をリセットします。
typescript// src/__tests__/isolation.test.ts
describe('テストの独立性', () => {
let database: Database;
beforeEach(async () => {
// 各テストの前にデータベースを初期化
database = await createTestDatabase();
await database.seed(); // 初期データを投入
});
afterEach(async () => {
// 各テストの後にクリーンアップ
await database.cleanup();
await database.close();
});
});
この構造により、各テストは常にクリーンな状態から開始され、前のテストの影響を受けません。
テストデータの分離
共有リソースを使う場合は、テストごとにユニークな識別子を使用します。
typescript// src/__tests__/data-isolation.test.ts
import { v4 as uuidv4 } from 'uuid';
describe('データ分離', () => {
test('ユーザー登録が成功する', async () => {
// テストごとにユニークなメールアドレスを生成
const uniqueEmail = `user-${uuidv4()}@example.com`;
const user = await createUser({
email: uniqueEmail,
name: '太郎',
});
expect(user.email).toBe(uniqueEmail);
});
});
ユニークな識別子により、並列実行時でもテスト間の衝突を防げます。
以下の図は、解決策の全体像を示しています。
mermaidflowchart TD
start["フレークテスト検出"]
analyze["原因分析"]
async_issue["非同期処理"]
random_issue["乱数依存"]
timer_issue["タイマー依存"]
state_issue["状態共有"]
async_sol["async/await<br/>waitFor<br/>タイマーモック"]
random_sol["Math.randomモック<br/>seedrandom<br/>依存注入"]
timer_sol["jest.useFakeTimers<br/>時間制御"]
state_sol["beforeEach/afterEach<br/>データ分離"]
retry["重試行設定"]
stable["安定したテスト"]
start --> analyze
analyze --> async_issue
analyze --> random_issue
analyze --> timer_issue
analyze --> state_issue
async_issue --> async_sol
random_issue --> random_sol
timer_issue --> timer_sol
state_issue --> state_sol
async_sol --> retry
random_sol --> retry
timer_sol --> retry
state_sol --> retry
retry --> stable
図の要点:
- まず原因を特定する
- 原因ごとに適切な解決策を適用
- 最後に重試行を追加して多層防御
- すべての対策が安定したテストに繋がる
具体例
実例 1:API テストの安定化
実際のプロジェクトで遭遇した、不安定な API テストを安定化させた事例を紹介します。
問題のあるコード
当初のテストコードは、以下のような構造でした。
typescript// src/__tests__/api-flaky.test.ts
describe('ユーザー API', () => {
test('ユーザー作成が成功する', () => {
const response = fetch('/api/users', {
method: 'POST',
body: JSON.stringify({
email: 'test@example.com',
name: '太郎',
}),
});
// エラー: Promise の待機なし
expect(response.status).toBe(201);
});
});
このテストは、以下の問題を抱えていました。
| # | 問題点 | 影響 |
|---|---|---|
| 1 | fetch の Promise を待機していない | 常に失敗または不安定 |
| 2 | メールアドレスが固定値 | 2 回目以降は重複エラー |
| 3 | 作成したユーザーのクリーンアップなし | テスト間で干渉 |
改善後のコード
問題を一つずつ解決していきます。まず、非同期処理の適切な待機を実装します。
typescript// src/__tests__/api-stable.test.ts (ステップ1)
describe('ユーザー API(改善版)', () => {
test('ユーザー作成が成功する', async () => {
// async/await で非同期処理を待機
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'test@example.com',
name: '太郎',
}),
});
expect(response.status).toBe(201);
});
});
次に、ユニークなメールアドレスを生成します。
typescript// src/__tests__/api-stable.test.ts (ステップ2)
import { v4 as uuidv4 } from 'uuid';
describe('ユーザー API(改善版)', () => {
test('ユーザー作成が成功する', async () => {
// テストごとにユニークなメールアドレス
const uniqueEmail = `test-${uuidv4()}@example.com`;
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: uniqueEmail,
name: '太郎',
}),
});
const user = await response.json();
expect(response.status).toBe(201);
expect(user.email).toBe(uniqueEmail);
});
});
最後に、クリーンアップ処理を追加します。
typescript// src/__tests__/api-stable.test.ts (ステップ3)
import { v4 as uuidv4 } from 'uuid';
describe('ユーザー API(改善版)', () => {
const createdUserIds: string[] = [];
afterEach(async () => {
// 作成したユーザーをすべて削除
for (const userId of createdUserIds) {
await fetch(`/api/users/${userId}`, {
method: 'DELETE',
});
}
createdUserIds.length = 0; // 配列をクリア
});
test('ユーザー作成が成功する', async () => {
const uniqueEmail = `test-${uuidv4()}@example.com`;
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: uniqueEmail,
name: '太郎',
}),
});
const user = await response.json();
createdUserIds.push(user.id); // クリーンアップ用に記録
expect(response.status).toBe(201);
expect(user.email).toBe(uniqueEmail);
});
});
これらの改善により、テストは完全に安定化しました。
実例 2:タイマー処理のテスト安定化
次は、タイマーを使った遅延処理のテストを安定化させた事例です。
問題のあるコード
元のコードは、実際の時間経過に依存していました。
typescript// src/utils/debounce.ts
export function debounce<
T extends (...args: any[]) => void
>(fn: T, delay: number): T {
let timeoutId: NodeJS.Timeout | null = null;
return ((...args: any[]) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => fn(...args), delay);
}) as T;
}
このデバウンス関数のテストは、実時間を待機する必要がありました。
typescript// src/__tests__/debounce-flaky.test.ts
test('デバウンスが動作する', (done) => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
debounced();
debounced();
debounced();
// 100ms 待機(実時間)
setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1); // 環境によって失敗
done();
}, 150);
});
このテストは、CI 環境の負荷によって失敗することがありました。
改善後のコード
Jest のフェイクタイマーを使用して、時間を制御します。
typescript// src/__tests__/debounce-stable.test.ts
describe('デバウンス(改善版)', () => {
beforeEach(() => {
// フェイクタイマーを有効化
jest.useFakeTimers();
});
afterEach(() => {
// 実際のタイマーに戻す
jest.useRealTimers();
});
test('短時間に複数回呼ばれても1回だけ実行される', () => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
// 3回連続で呼び出し
debounced();
debounced();
debounced();
// まだ実行されていない
expect(fn).not.toHaveBeenCalled();
// 時間を100ms進める(実時間は経過しない)
jest.advanceTimersByTime(100);
// 1回だけ実行された
expect(fn).toHaveBeenCalledTimes(1);
});
});
フェイクタイマーにより、テストは瞬時に完了し、環境に依存しなくなりました。
さらに、複雑なシナリオもテストできます。
typescript// src/__tests__/debounce-stable.test.ts
test('遅延時間内の呼び出しはタイマーがリセットされる', () => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
debounced();
// 50ms経過
jest.advanceTimersByTime(50);
expect(fn).not.toHaveBeenCalled();
// 再度呼び出し(タイマーリセット)
debounced();
// さらに50ms経過(合計100ms)
jest.advanceTimersByTime(50);
// まだ実行されない(最後の呼び出しから50msしか経過していない)
expect(fn).not.toHaveBeenCalled();
// さらに50ms経過(最後の呼び出しから100ms)
jest.advanceTimersByTime(50);
// 実行される
expect(fn).toHaveBeenCalledTimes(1);
});
この改善により、テストの実行時間が大幅に短縮され、安定性も向上しました。
実例 3:乱数を使った ID 生成の安定化
最後に、乱数を使った ID 生成処理のテストを安定化させた事例です。
問題のあるコード
元のコードは、Math.random() を直接使用していました。
typescript// src/utils/id-generator-old.ts
export function generateUserId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `user_${timestamp}_${random}`;
}
このコードのテストは、予測不可能な値を検証する必要がありました。
typescript// src/__tests__/id-generator-flaky.test.ts
test('ユーザー ID が生成される', () => {
const id = generateUserId();
// 正確な値を検証できない
expect(id).toMatch(/^user_\d+_[a-z0-9]{9}$/);
// パターンマッチのみで、実際の値は検証できない
});
このテストは、ID の形式しか検証できず、実装の詳細な動作を保証できませんでした。
改善後のコード(依存注入パターン)
乱数生成器とタイムスタンプ生成器を注入可能にします。
typescript// src/utils/id-generator-new.ts
// 乱数生成器のインターフェース
export interface RandomSource {
generate(): number;
}
// 時刻取得のインターフェース
export interface TimeSource {
now(): number;
}
// デフォルト実装
export class DefaultRandomSource implements RandomSource {
generate(): number {
return Math.random();
}
}
export class DefaultTimeSource implements TimeSource {
now(): number {
return Date.now();
}
}
// ID生成クラス
export class UserIdGenerator {
constructor(
private randomSource: RandomSource = new DefaultRandomSource(),
private timeSource: TimeSource = new DefaultTimeSource()
) {}
generate(): string {
const timestamp = this.timeSource.now();
const random = this.randomSource
.generate()
.toString(36)
.substr(2, 9);
return `user_${timestamp}_${random}`;
}
}
テストでは、固定値を返すモック実装を注入します。
typescript// src/__tests__/id-generator-stable.test.ts
import {
UserIdGenerator,
RandomSource,
TimeSource,
} from '../utils/id-generator-new';
// テスト用の固定値を返す実装
class FixedRandomSource implements RandomSource {
constructor(private value: number) {}
generate(): number {
return this.value;
}
}
class FixedTimeSource implements TimeSource {
constructor(private value: number) {}
now(): number {
return this.value;
}
}
describe('ユーザーID生成(改善版)', () => {
test('指定した値から ID が生成される', () => {
// 固定値のソースを注入
const randomSource = new FixedRandomSource(0.123456789);
const timeSource = new FixedTimeSource(1700000000000);
const generator = new UserIdGenerator(
randomSource,
timeSource
);
const id = generator.generate();
// 正確な値を検証できる
expect(id).toBe('user_1700000000000_0dkif0yoe');
});
test('異なる乱数値で異なる ID が生成される', () => {
const timeSource = new FixedTimeSource(1700000000000);
const generator1 = new UserIdGenerator(
new FixedRandomSource(0.1),
timeSource
);
const generator2 = new UserIdGenerator(
new FixedRandomSource(0.9),
timeSource
);
const id1 = generator1.generate();
const id2 = generator2.generate();
expect(id1).not.toBe(id2);
expect(id1).toBe('user_1700000000000_1llllllle');
expect(id2).toBe('user_1700000000000_w6666666e');
});
});
この設計により、テストは完全に決定的になり、実装の詳細な動作を検証できるようになりました。
以下の図は、これらの実例で適用した改善パターンの関係性を示しています。
mermaidflowchart TB
subgraph Example1["実例1: API テスト"]
api_before["問題: Promise未待機<br/>固定メールアドレス<br/>クリーンアップなし"]
api_after["解決: async/await<br/>UUID生成<br/>afterEachクリーンアップ"]
api_before --> api_after
end
subgraph Example2["実例2: タイマー"]
timer_before["問題: 実時間待機<br/>環境依存"]
timer_after["解決: jest.useFakeTimers<br/>時間制御"]
timer_before --> timer_after
end
subgraph Example3["実例3: 乱数ID"]
id_before["問題: Math.random直接使用<br/>予測不可能"]
id_after["解決: 依存注入<br/>モック実装"]
id_before --> id_after
end
pattern["共通パターン:<br/>依存性の注入と制御"]
api_after --> pattern
timer_after --> pattern
id_after --> pattern
図の要点:
- 3 つの実例すべてで依存性の制御がキーとなる
- 外部要因(時間、乱数、ネットワーク)を注入可能にする
- テスト時はモック実装で決定的な動作を保証
ベストプラクティスのチェックリスト
実務でフレークテストを防ぐために、以下のチェックリストを活用してください。
| # | 項目 | チェック |
|---|---|---|
| 1 | すべての非同期処理で async/await を使用している | □ |
| 2 | Math.random() や Date.now() を直接使用していない | □ |
| 3 | タイマー処理では jest.useFakeTimers() を使用している | □ |
| 4 | テストごとに beforeEach / afterEach でクリーンアップしている | □ |
| 5 | テストデータはユニークな識別子を使用している | □ |
| 6 | 外部 API 呼び出しはモックまたはスタブ化している | □ |
| 7 | 重要なテストには重試行を設定している | □ |
| 8 | CI/CD 環境でテストを複数回実行して安定性を確認している | □ |
このチェックリストを開発フローに組み込むことで、フレークテストの発生を未然に防げます。
まとめ
フレークテストは、開発チームの生産性と士気に深刻な影響を与える問題です。しかし、適切な手法を用いることで、確実に撲滅できるのです。
本記事で紹介した解決策を振り返ってみましょう。
まず、重試行メカニズムの導入により、一時的な不安定性を吸収できます。jest-retries を活用することで、テストレベルやスイートレベルで柔軟に重試行を設定できました。
次に、乱数の固定により、予測不可能な動作を排除しました。Math.random() のモック化や seedrandom の活用、そして依存注入パターンにより、テストの再現性を確保できます。
さらに、非同期処理の適切な待機が重要でした。async/await の正しい使用、waitFor によるポーリング、そして jest.useFakeTimers() によるタイマー制御により、タイミング依存の問題を解決できたのです。
最後に、テスト間の独立性を確保することで、状態共有による不安定性を排除しました。beforeEach / afterEach の活用とユニークなテストデータの生成により、並列実行時でも安定したテストを実現できます。
これらの手法を組み合わせることで、フレークテストのない安定したテスト環境を構築できるでしょう。テストへの信頼が回復すれば、開発速度は向上し、品質も確保されます。
フレークテストとの戦いは、一度の対処で終わるものではありません。継続的に監視し、新たなフレークテストが発生したらすぐに原因を特定して対処する文化を作ることが大切ですね。
あなたのプロジェクトでも、今日からフレークテスト撲滅作戦を始めてみませんか。
関連リンク
articleJest のフレークテスト撲滅作戦:重試行・乱数固定・リトライ設計の実務
articleJest の層別テスト設計:単体/契約/統合をディレクトリで整然と運用
articleJest moduleNameMapper 早見表:パスエイリアス/静的アセット/CSS を一網打尽
articleJest の ESM/NodeNext 設定完全ガイド:transformIgnorePatterns と resolver 設計
articleJest の DOM 環境比較:jsdom vs happy-dom — 互換性・速度・安定性
articleJest の “Cannot use import statement outside a module” を根治する手順
articleHaystack で最小の検索 QA を作る:Retriever + Reader の 30 分ハンズオン
articleJest のフレークテスト撲滅作戦:重試行・乱数固定・リトライ設計の実務
articleGitHub Copilot セキュア運用チェックリスト:権限・ポリシー・ログ・教育の定着
articleGrok で社内 FAQ ボット:ナレッジ連携・権限制御・改善サイクル
articleGitHub Actions ランナーのオートスケール運用:Kubernetes/actions-runner-controller 実践
articleClips AI で書き出しが止まる時の原因切り分け:メモリ不足・コーデック・権限
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来