フレーキーテスト探偵 (Flaky Test Detective)
同じコードで成功したり失敗したりするテストを捕まえ、決定論的に書き換えましょう。チームが赤いビルドを無視する習慣がつく前に。
フレーキーテストはテストがないことより悪いです。一度失敗し、誰かがCIを再実行すると成功し、誰もが「赤は壊れていることを意味しない」と学習します。やがて、本当のリグレッションが「再実行すればいい」という考えの陰をすり抜けます。その原因は、ほとんどの場合、テスト対象のコードではなく、テストが制御していない何か(時計、乱数シード、テストの実行順序、データベースの共有行、実際のネットワーク呼び出し、マップの反復順序など)に対する隠れた依存関係です。このスキルはその隠れた入力を特定し、なぜテストを非決定論的にするのかを説明し、テストを制御可能な形に書き換えます。
使用すべき状況
- テストが断続的に失敗する — コード変更なしに、ある実行では赤、次の実行では緑。
- テストがローカルでは緑だがCIでは赤(またはその逆)、あるいは並列実行や特定の順序でのみ失敗する。
- 誰かが*「とにかく再実行しよう」、「フレーキーだ」、「リトライで通る」*と言っている。
- テストを作成またはレビューしており、非決定性がコミットされる前に検出したい。
- スイートを強化したい — 顕在化する前に潜在的なフレーキネスを見つけたい。
テストが毎回失敗するなら、それはフレーキーではありません — 実際の失敗か本当のバグです。コードを修正しましょう。決して、テストを安定化させるためにリトライやsleepを追加して隠蔽しないでください。それはシグナルを埋もれさせます。目標は決定論的なテストであって、「たいていは緑」になるテストではありません。
ワークフロー
-
コンテキストを確立する。 言語/フレームワーク/ランナー(pytest、Jest/Vitest、RSpec/Minitest、Go
testing、JUnit、PHPUnitなど)と、どこでフレーキーになるか(ローカル、CI、並列実行時のみ、特定の順序でのみ、特定の時刻のみ)を特定します。どこでいつフレーキーになるかが、原因を突き止める最大の手がかりです。 -
非決定性を再現する。 オンデマンドで失敗させられないフレーキーテストは、修正されたのではなく、単に静かになっただけです。強制的に実行します:多数回実行する、ランダムな順序で実行する、単独で実行するのとフルスイートとで比較する、1ワーカーに固定する、時計/タイムゾーンをずらす。
reference/detection.mdにフレームワークごとの正確なコマンドがあります。 -
根本原因を分類し、重要度を割り当てます:
- 🔴 フレーキー — 具体的な非決定論的入力がアサーションに到達する;いずれ失敗する。決定論的にする必要があります。
- 🟡 潜在性 — 原理的には非決定論的だが現在はマスクされている(たまたま安定しているシード、たまたま成立する順序、十分に速いCI)。それを壊す条件を特定します。
- 🟢 決定論的 — テストは入力を制御している。変更不要。
-
根本原因を修正する — ごまかさない。 🔴/🟡 それぞれに対して、以下を提供します:
- 非決定論的である理由 — 正確な制御されていない入力と、それがどのようにアサーションに到達するか、
- 決定論的な書き換えを実行可能なコード(フレームワーク慣用的に)、そして
- 結果(例:「順序依存→分離」または「20回に1回フレーキー→1000回緑」)。
-
レポートは末尾の出力テンプレートを使用します。🔴を先頭にします。
言語および原因固有の詳細については、目の前のテストに関連するリファレンスファイルをロードしてください:
reference/root-causes.md— 8つの根本原因ファミリーの詳細:各原因がどのようにテストをフレーキーにするか、検出方法、および標準的な修正方法。reference/frameworks.md— フレームワークごとのメカニズム:pytest、Jest/Vitest、RSpec/Minitest、Go、JUnit、PHPUnitにおける時計制御、RNGシード、テスト順序のランダム化/固定、状態の分離、ネットワークモック。reference/detection.md— オンデマンドでフレーキネスを再現する方法:再実行ループ、順序ランダム化、分離/二分法、並列実行、CIシグナルの読み取り。reference/patterns.md— 決定論的テストの原則:密閉性、時計/RNG/ネットワーク/順序の制御、テストごとの分離、検出→修正→隔離のライフサイクル。
目の前のフレームワークのリファレンスを読んでから作業してください。推測はしないでください — メカニズムは異なります(時間の固定、シード、順序制御はpytest、Jest、Goで完全に異なります)。
根本原因カタログ(高頻度の原因)
「制御されていない入力」= テストの結果が依存するもので、テストが固定していないもの。フレーキネスは常に、制御されていない入力がアサーションに到達することです。
| # | 根本原因 | 非決定論的である理由 | 決定論的な修正 |
|---|---|---|---|
| 1 | テスト順序依存 / 共有可変状態 | テストが別のテストが残した状態(グローバル、クラス属性、モジュールシングルトン、ロールバックされていない行)に依存する。ある順序では成功し、別の順序では失敗する。 | 分離:テストごとに新しいフィクスチャ、teardownでグローバルをリセット、各テストをトランザクションロールバックでラップ。CIで順序をランダム化して検出。 |
| 2 | 時刻とタイムゾーン | テストまたはコード内のnow()/Date.now()/today() — 深夜、月/年の境界、DST、またはCIのタイムゾーンで失敗する。 | 時計を固定(freezegun/time-machine、Jest fake timers、timecop、Goの注入可能クロック);TZを固定;実際の壁時計に対してアサートしない。 |
| 3 | シードされていないランダム性 | random()、uuid4()、faker、シャッフルされたデータ — まれな値がN回に1回アサーションをトリップする。 | RNGにシードを設定してテストごとに決定論的に;ID/フェイクを注入;特定のランダム値ではなくプロパティに対してアサート。 |
| 4 | 非同期の競合状態 / sleepベースの待機 | sleep(100)してからアサート — CIが遅いと失敗;未完了のPromise;コールバックが実行される前にアサート;イベントの順序。 | 期間ではなく条件を待つ(waitFor/awaitで実際のシグナル);すべてのPromiseをawait;fake timersを使用してスケジューリングを制御。 |
| 5 | 実際のネットワーク / 外部サービス | 実際のHTTP/API/DNS呼び出し — レイテンシ、レート制限、ダウンタイム、データ変更で失敗。 | 境界でモック(responses/respx、nock/msw、VCR/WireMock);ユニットテストでは決して実際のネットワークにアクセスしない。 |
| 6 | 順序なしコレクションのアサーション | set/dict/map、ORDER BYなしのDBクエリ、os.listdir、または並行実行結果からの特定の順序をアサート。 | 比較前にソート、集合等価性/包含をアサート、または明示的なORDER BYを追加。偶発的な順序に依存しない。 |
| 7 | 漏洩したグローバル状態 / リソースの漏れ | 環境変数、シングルトン、モジュールキャッシュ、カレントディレクトリ、開いたままの接続/ファイル/ポートが変更され、復元されない — 後続のテストを汚染する。 | 各テストの前後で保存/復元(フィクスチャ/beforeEach/t.Cleanup);すべてのリソースをクローズ;自動アンドゥでモンキーパッチ。 |
| 8 | 厳しすぎるタイムアウト / パフォーマンスの仮定 | 高速なラップトップでは成立するが、負荷の高いCIランナーでは成立しないハードコードされたタイムアウトまたはassert elapsed < X。 | 正しさのテストから壁時計のパフォーマンスアサーションを削除;タイムアウトが必要な場合は、余裕を持たせて条件ベースにする。 |
| 9 | 浮動小数点の完全一致 | 算術演算後のassert result == 0.3 — 丸めにより時々等しくならない。 | 許容範囲を持ってアサート(pytest.approx、toBeCloseTo、イプシロン比較)。 |
| 10 | ロケール / 環境の仮定 | 数値/日付/文字列の書式、大文字小文字の変換、パス区切り文字、またはマシン/CI間で異なるファイルシステムの大文字小文字の区別。 | テスト内でロケール/エンコーディングを固定;ロケール非依存の書式を使用;OS固有のパスをアサートしない。 |
| 11 | 共有フィクスチャ / DBの分離不足 | テストがデータベース、キャッシュ、tempディレクトリを共有し、並列実行時に互いに干渉する。 | テストごとのトランザクション/ロールバック、ワーカーごとの一意のスキーマ/名前空間、テストごとのtmp_path;共有書き込み可能フィクスチャなし。 |
| 12 | テスト対象コード内の隠された並行性 | バックグラウンドスレッド、タイマー、非同期タスクがアサーションに対して予測不能に完了する(または完了しない)。 | 境界をawait可能/注入可能にする;キューを決定論的にドレイン/フラッシュ;制御可能なexecutor/クロックを注入。 |
🟡 層は、フレーキネスが顕在化する前に潜む場所です:たまたま最後に実行されるため順序依存であるテスト、現在安定しているシード、現在十分に長いsleep。それを🔴に変える条件を特定してください — それが待っている修正です。
ガイドライン
- フレーキネスは制御されていない入力であって、不運ではない。 すべてのフレーキーテストは、固定していない何か(時計、シード、順序、ネットワーク、共有行)に依存しています。その入力を見つけて制御してください — 固定する、シードする、分離する、モックする。
- 修正する前に再現する。 オンデマンドで失敗させられないなら(再実行ループ、ランダム順序、分離)、原因を見つけたのではなく、隠したのです。
- テストは自身の状態を所有しなければならない。 単独で、任意の順序で、並列で、千回実行しても成功するべきです。セットアップが必要なものを作成し、ティアダウンは触れたものを復元します。
- 4つのよくある容疑者を制御する。 時計、ランダム性、ネットワーク、順序がほとんどのフレーキネスの原因です — それぞれをデフォルトで決定論的にします。
- 時間ではなく条件を待つ。
sleep(n)は、遅いランナーでは負ける競争です。実際のシグナルを待つか、余裕を持った上限で条件をポーリングします。 - 決してリトライでフレーキネスを修正しない。 リトライや包括的な
sleepは、目に見える欠陥を沈黙の欠陥に変え、スイートの信頼性を破壊します。 - 削除ではなく隔離する。 すぐに修正できないフレーキーテストは、追跡チケット付きでマーク(スキップ/隔離)してブロックを防ぎますが、維持し、壊れているというシグナルを保持します。
- 適切なサイズにする。 本当に順序独立で密閉されたテストにはこれらは不要です — すでに入力を制御しているテストに決定性劇場を追加しないでください。
出力テンプレート
## Flaky Test Review
**Stack:** <言語 / フレームワーク / ランナー> · **Flakes:** <CIのみ / 並列実行 / ランダム順序 / 時刻X> · **Tests reviewed:** <n>
### 🔴 <ファイル::テスト名> — FLAKY
**Root cause:** <順序依存 / 時刻 / ランダム性 / 非同期競合 / ネットワーク / 順序付け / グローバル状態 / タイムアウト / …>
**Why non-deterministic:** <正確な制御されていない入力とアサーションへの到達方法>
**Reproduce:** <オンデマンドで失敗させるコマンド — ランダム順序、再実行ループ、分離、クロックシフト>
**Deterministic rewrite:**
<実行可能でフレームワーク慣用的なコード>
**Result:** <例:「順序依存→分離;1000回のランダム順序実行で緑」>
### 🟡 <ファイル::テスト> — LATENT
**Root cause / Why it's currently masked / The condition that would flip it to flaky / Recommendation**
### 🟢 <ファイル::テスト> — DETERMINISTIC
<1行:入力を制御している — 変更不要>
---
**Summary:** <n flaky, n latent.> <最悪の制御されていない入力を1行で。> <結論:マージしても安定、またはテストXを先に修正。>
常に決定論的な書き換えと再現コマンドを提供してください — 「これはフレーキーだ」と言うだけで、制御された入力の修正とそれを証明する方法を提供しないレビューは、難しい部分を返しているだけです。制御されていない入力に名前を付けることこそが、このスキルが提供する専門知識です。