第52章:遅い/不安定(フレーク)を潰す手順🐢💥

テストって、速い&信頼できるほど気持ちよく回るよね🙂 この章は「テストが遅い…」「たまに落ちる…」を、手順で確実に直せるようになる回だよ〜!💪💕
🎯 目的
- 「遅いテスト」「フレーク(たまに落ちるテスト)」を 見つけて→原因を絞って→直して→再発防止できるようになる🧠✨
- 最後に 10回回して0落ちを達成する✅🔁
📚 学ぶこと(この章の武器)🧰
1) “遅い”の正体
-
だいたいコレ👇
- 💤 本物の待ち時間(
setTimeout/ リトライ待ち / backoff) - 🌐 外部依存(HTTP / DB / ファイル / OS)
- 🧱 重い準備(巨大データ生成・初期化)
- 🧵 並列が裏目(ロック・競合・リソース奪い合い)
- 💤 本物の待ち時間(
2) “フレーク”の定番犯人
- 🎲 乱数(
Math.random) - ⏰ 時刻・タイムゾーン(
Date.now()/new Date()) - 🧺 共有状態(グローバル変数・シングルトン・キャッシュ)
- 🧵 並列実行による干渉(同じポート/同じファイル名/同じDB)
- 🌐 ネットワークや実行環境の揺れ
3) Vitestの便利オプション(この章の主役)
- 失敗を再現しやすくする:
--sequence.shuffleと--sequence.seed🌀🌱 (main.vitest.dev) - フレーク検出に使える:
--retry(※“直すまでの一時しのぎ”として)🔁 (Vitest) - 並列が原因か切り分け:
--no-file-parallelism(ファイル並列を止める)🧵✂️ (Vitest) - タイマー待ちを消す:
vi.useFakeTimers()/vi.advanceTimersByTime()⏱️✨ (Vitest) - “今日”問題を消す:
vi.setSystemTime()📅🧊 (Vitest)
🧪 手を動かす:遅い&フレークを「わざと作って」直す🧪✨
ここから3つのミニ実験で、直し方を体に入れよ〜!💕
実験A:遅いテスト(本物の待ち時間)を即終了させる🐢➡️⚡
❌ 悪い例:本当に待ってる(遅い)
import { test, expect } from 'vitest'
function retry3Times(fn: () => boolean) {
let ok = fn()
if (ok) return true
// ほんとは待つ系(遅くなる原因)
// 例として 100ms * 2 回待つ想定
// 実装はわざと雑にしてるよ
return new Promise<boolean>((resolve) => {
setTimeout(() => {
ok = fn()
if (ok) return resolve(true)
setTimeout(() => resolve(fn()), 100)
}, 100)
})
}
test('retry3Times: 最後は成功する', async () => {
let count = 0
const result = await retry3Times(() => ++count >= 3)
expect(result).toBe(true)
})
これ、テストが増えると地獄…😵💫(待ちが積み上がる)
✅ 良い例:Fake Timersで“待ち時間をスキップ”⏱️✨
import { test, expect, vi } from 'vitest'
function retry3Times(fn: () => boolean) {
let ok = fn()
if (ok) return Promise.resolve(true)
return new Promise<boolean>((resolve) => {
setTimeout(() => {
ok = fn()
if (ok) return resolve(true)
setTimeout(() => resolve(fn()), 100)
}, 100)
})
}
test('retry3Times: 最後は成功する(Fake Timers)', async () => {
vi.useFakeTimers() // ✅ タイマーを偽物に
let count = 0
const p = retry3Times(() => ++count >= 3)
// ✅ “時間を進める”だけでOK(実時間は待たない)
vi.advanceTimersByTime(200)
await expect(p).resolves.toBe(true)
vi.useRealTimers() // ✅ 後片付け
})
ポイント💡
vi.useFakeTimers()でタイマーを偽物にしてvi.advanceTimersByTime(ms)で 時間を瞬間移動させる🌟 (Vitest)
実験B:フレーク(乱数)を“注入”で固定する🎲🚫
❌ 悪い例:乱数に頼ってる(たまに落ちる)
import { test, expect } from 'vitest'
function pickBonus() {
return Math.random() < 0.1 ? 'BIG' : 'SMALL'
}
test('pickBonus: BIGが出る', () => {
expect(pickBonus()).toBe('BIG') // 😱 たまにしか出ない
})
✅ 良い例:乱数源を注入して固定🎯
import { test, expect } from 'vitest'
type Rng = () => number
function pickBonus(rng: Rng) {
return rng() < 0.1 ? 'BIG' : 'SMALL'
}
test('pickBonus: BIGが出る(固定)', () => {
const rng = () => 0.05 // ✅ いつでもBIG
expect(pickBonus(rng)).toBe('BIG')
})
test('pickBonus: SMALLが出る(固定)', () => {
const rng = () => 0.5 // ✅ いつでもSMALL
expect(pickBonus(rng)).toBe('SMALL')
})
これで「たまに落ちる」が消える🎉✨ (“運ゲー”をやめて、“仕様”に戻す感じ🙂)
実験C:順序依存フレークを「シャッフル」で炙り出して直す🌀🔥
❌ 悪い例:共有状態が残ってる
import { test, expect } from 'vitest'
const cache: string[] = []
test('キャッシュに追加できる', () => {
cache.push('A')
expect(cache.length).toBe(1)
})
test('キャッシュは空である', () => {
expect(cache.length).toBe(0) // 😱 実行順で死ぬ
})
✅ まず炙り出す:テスト順をシャッフル🌀
--sequence.shuffleで順番をランダムにできるよ (main.vitest.dev)- さらに
--sequence.seed=1000みたいに seed固定で再現できる🌱 (main.vitest.dev)
(たとえば)
npx vitest run --sequence.shuffle --sequence.seed=1000
✅ 直し方:beforeEach で毎回リセット(テスト独立性)
import { test, expect, beforeEach } from 'vitest'
let cache: string[] = []
beforeEach(() => {
cache = [] // ✅ 毎回まっさらに
})
test('キャッシュに追加できる', () => {
cache.push('A')
expect(cache.length).toBe(1)
})
test('キャッシュは空である', () => {
expect(cache.length).toBe(0)
})
🧪 切り分けテンプレ:遅い/フレークを見つけたらこの順でOK✅
Step 1:まず「再現」させる🔁
-
フレークっぽいなら
--sequence.shuffle --sequence.seed=...で順序問題を炙る🌀🌱 (main.vitest.dev)--retry 20で「たまに落ちる」を表に出す(ただし直すのが本命)🔁 (Vitest)
Step 2:並列が原因か切る🧵✂️
-
まずは安全に ファイル並列を止めて様子を見る
--no-file-parallelism(Vitest)
-
これで安定するなら「競合・共有リソース」が本命!
Step 3:時間・乱数・日付を固定する⏰🎲📅
- タイマー待ち →
vi.useFakeTimers()+vi.advanceTimersByTime()⏱️ (Vitest) - “今日”依存 →
vi.setSystemTime()📅 (Vitest) - 乱数依存 → 乱数源を注入して固定🎯
Step 4:外部依存を“境界で偽物化”🌐🚫
- HTTP/DB/ファイル/OS は、境界でラップしてユニットテストでは偽物にする(外部は統合テストに寄せる) (この方針が遅さ&フレークの一番の予防になるよ🙂)
🧪 “遅いテスト”高速化の定番レシピ5️⃣🐢➡️⚡
-
sleepを消す(Fake Timersへ)⏱️✨ (Vitest)
-
巨大データを減らす(必要最小のArrangeへ)🧸
-
重い初期化を分離(境界でラップ)🚪
-
後片付け漏れをなくす(リソースclose忘れは遅さ&フレークの元)🧹
-
並列設定を見直す(安定優先なら並列を落とす/速度優先ならpool検討)🧵
poolはforks/threads/...を選べるよ(速度の代わりに相性問題もある) (Vitest)
🤖 AIの使いどころ(“診断”だけAIにやらせると強い)🕵️♀️✨
そのままコピペで使えるプロンプト例だよ💕
① 遅い原因の推理
- 「このテストが遅いです。原因候補を 外部依存/待ち時間/重い準備/並列競合 に分類して、最小修正→中修正→大修正の順に3案ください」
② フレーク原因チェック
- 「この“たまに落ちる”テストの原因候補を 時間/乱数/共有状態/順序/並列/外部 の観点で列挙して、再現手順(seed固定など)も提案して」
③ 直した後の再発防止
- 「この修正で再発しそうなポイントを指摘して、予防ルールを5つにまとめて」
※注意💡:AIの提案は“答え”じゃなくて“仮説”!最後はテストで確定ね🙂🤝
✅ チェック(合格ライン)🎉
-
--sequence.shuffle --sequence.seed=1000で回しても落ちない🌀🌱 (main.vitest.dev) -
--retry 20をかけても落ちない(= フレークっぽさが消えた)🔁 (Vitest) - 10回連続でグリーン✅🔁
- “待ち時間テスト”が Fake Timers に置き換わってる⏱️✨ (Vitest)
- “今日/乱数/共有状態”が固定 or 注入で制御できてる📅🎲🧼 (Vitest)
🧾 この章の提出物(作ると強い!)📌
-
「遅い/フレーク診断メモ」(テンプレ)
- 症状:遅い or たまに落ちる
- 再現コマンド:shuffle/seed/retry/並列OFF
- 原因:時間/乱数/共有/外部/並列 のどれ?
- 対策:最小→中→大(3段)
- 再発防止:ルール3つ
次の章に進む前に、もし「最近あなたのプロジェクトで遅い/落ちるテスト」が1本でもあれば、そのテストコード(1ファイルでOK)貼ってくれたら、この手順で一緒に“最短ルートで安定化”まで持っていけるよ🧪💞