Flakyテストとは? 通ったり落ちたりする不安定なテストの原因と潰し方

目次

「さっきCI通ったのに、もう1回回したら落ちた」

PRを出してCIを回す。落ちた。コードは変えてない。もう一度回す。通った。「何だったんだ…?」と首をかしげながらマージ。翌日また別のPRで同じテストが落ちる。

この「通ったり落ちたりするテスト」のことを、エンジニアの現場ではFlaky(フレイキー)テストと呼びます。放置するとCIへの信頼が崩壊し、「とりあえず再実行」が常態化して、テストが本来果たすべき「バグを防ぐ」という役割を失います。

本記事では、Flakyテストの正体、よくある原因パターン、見つけ方と潰し方を解説します。

Flakyテストとは?

Flaky(フレイキー)は英語で「剥がれやすい」「不安定な」という意味。Flakyテストとは、コードを一切変更していないのに、実行するたびに成功したり失敗したりするテストのことです。

【正常なテスト】
1回目: ✅ Pass
2回目: ✅ Pass
3回目: ✅ Pass
→ コードが正しければ常にPass

【Flakyテスト】
1回目: ✅ Pass
2回目: ❌ Fail  ← コード変えてないのに!
3回目: ✅ Pass
→ 結果がランダム。信頼できない

Flakyテストの厄介さは、「本当にバグで落ちたのか、Flakyで落ちたのか区別がつかない」こと。結果として「とりあえずRetry」が習慣化し、本物のバグを見逃すリスクが高まります。

よくある原因5パターン

原因1:テスト間の依存(実行順序に依存している)

テストAがDBにデータを入れて、テストBがそのデータを前提に動いている。テストの実行順序が変わると、Bが先に走ってデータがなくて落ちる。

// ❌ テストAが作ったデータに依存している
it('ユーザー一覧を取得できる', async () => {
  // テストAで作られたユーザーがDBにいる前提
  const res = await request(app).get('/api/users');
  expect(res.body.length).toBe(3); // テストAが先に走らないと0件
});

// ✅ 各テストが独立してデータを用意する
it('ユーザー一覧を取得できる', async () => {
  await createTestUsers(3); // 自分でデータを用意
  const res = await request(app).get('/api/users');
  expect(res.body.length).toBe(3);
});

原因2:時刻に依存している

new Date()Date.now()を直接使っているテストは、実行タイミングによって結果が変わります。深夜0時をまたぐとき、月末、うるう年などで突然落ちるパターン。

// ❌ 現在時刻に依存
it('今日の日付が表示される', () => {
  expect(getGreeting()).toBe('2026年6月6日'); // 明日になったら落ちる
});

// ✅ 時刻をモックする
it('指定日の日付が表示される', () => {
  vi.setSystemTime(new Date('2026-06-06'));
  expect(getGreeting()).toBe('2026年6月6日');
  vi.useRealTimers();
});

原因3:非同期処理の待ちが不十分

APIレスポンスやDOMの更新を待たずにアサーションしている。ローカルでは速くて通るが、CIのマシンが遅いと間に合わず落ちる。

// ❌ 固定時間のsleep(環境によって間に合わない)
await new Promise(r => setTimeout(r, 500));
expect(screen.getByText('完了')).toBeInTheDocument();

// ✅ waitForで条件が満たされるまで待つ
await waitFor(() => {
  expect(screen.getByText('完了')).toBeInTheDocument();
});

原因4:外部サービスへの依存

テスト中に本物のAPIやDBに接続している。外部サービスが遅い、一時的に落ちている、レート制限に引っかかる、などの理由でテストが不安定になります。外部依存はモックで切り離すのが基本。

原因5:ランダムなテストデータ

Fakerなどでランダムにデータを生成している場合、特定のパターン(空文字、特殊文字、境界値)が生成された時だけ落ちることがあります。再現が難しく、最も厄介なFlaky。

見つけ方と潰し方

見つけ方:同じテストを複数回実行する

Flakyテストは1回の実行では見つかりません。同じテストを繰り返し実行して、結果がブレるかチェックします。

# Vitest: 同じテストを10回実行
for i in $(seq 1 10); do npx vitest run --reporter=verbose; done

# Jest: 同様に繰り返し
for i in $(seq 1 10); do npx jest --verbose; done

# Pest (PHP): 同様に
for i in $(seq 1 10); do ./vendor/bin/pest; done

10回中1回でも落ちればFlaky確定です。

潰し方:原則は「テストの独立性を確保する」

  • 各テストが自分でデータを用意し、終わったら片付ける。他のテストの結果に依存しない
  • 時刻・乱数はモックするvi.setSystemTime()や固定シードを使う
  • 非同期はwaitForで待つsetTimeoutの固定秒数に頼らない
  • 外部APIはモックで切り離す。テスト中にネットワークを叩かない
  • すぐ直せないなら.skipで一時的に無効化。Flakyを放置して「Retry文化」を作るのが一番まずい

テストの品質を高める関連記事

まとめ:Flakyテストは「テストスイートの癌」

  • Flakyテストとは、コードを変えていないのに通ったり落ちたりする不安定なテスト
  • よくある原因:テスト間の依存、時刻依存、非同期の待ち不足、外部サービス依存、ランダムデータ
  • 「とりあえずRetry」は最悪の対処。本物のバグを見逃す文化を作ってしまう
  • 原則は「各テストを独立させる」。データ・時刻・外部依存をモックで制御する
  • すぐ直せないなら.skipで無効化。Flakyを残すより、テスト数が減る方がマシ

Flakyテストが1つあるだけで、チームの「CIを信頼する文化」が壊れます。テストが落ちた時に「あー、またあのFlakyか」と思った瞬間、テストスイート全体の意味が薄れる。見つけたら最優先で潰す。それがテストの信頼性を守る唯一の方法です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次