「さっき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; done10回中1回でも落ちればFlaky確定です。
潰し方:原則は「テストの独立性を確保する」
- 各テストが自分でデータを用意し、終わったら片付ける。他のテストの結果に依存しない
- 時刻・乱数はモックする。
vi.setSystemTime()や固定シードを使う - 非同期は
waitForで待つ。setTimeoutの固定秒数に頼らない - 外部APIはモックで切り離す。テスト中にネットワークを叩かない
- すぐ直せないなら
.skipで一時的に無効化。Flakyを放置して「Retry文化」を作るのが一番まずい
テストの品質を高める関連記事
- Claude Codeとは?AI搭載のコーディングアシスタントを徹底解説 – Flakyテストの原因特定や修正案の相談をAIに任せる活用法
- 「それ、上げちゃダメ!」GitHub管理で絶対守るべきセキュリティルールと対処法 – CI/CDのテスト自動実行とブランチ保護の基本
- コマンドラインの基本と活用方法【初心者エンジニア向け】 – テストコマンドの実行やループ実行に必要なCLI操作
- リリース前に必ず確認!バイブコーディング&非エンジニア向けWebアプリ安全チェックリスト – テストの安定性を含むリリース前の品質チェック
まとめ:Flakyテストは「テストスイートの癌」
- Flakyテストとは、コードを変えていないのに通ったり落ちたりする不安定なテスト
- よくある原因:テスト間の依存、時刻依存、非同期の待ち不足、外部サービス依存、ランダムデータ
- 「とりあえずRetry」は最悪の対処。本物のバグを見逃す文化を作ってしまう
- 原則は「各テストを独立させる」。データ・時刻・外部依存をモックで制御する
- すぐ直せないなら
.skipで無効化。Flakyを残すより、テスト数が減る方がマシ
Flakyテストが1つあるだけで、チームの「CIを信頼する文化」が壊れます。テストが落ちた時に「あー、またあのFlakyか」と思った瞬間、テストスイート全体の意味が薄れる。見つけたら最優先で潰す。それがテストの信頼性を守る唯一の方法です。
