Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
不穩定的測試常常用任意的延遲來猜測時機。這會造成競態條件:測試在快的機器上通過,但在負載或 CI 下失敗。
Core principle: Wait for the actual condition you care about, not a guess about how long it takes.
核心原則: 等待你真正關心的條件,而不是猜測要花多久時間。
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}
Use when:
- Tests have arbitrary delays (setTimeout, sleep, time.sleep())
- Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
適用時機:
- 測試含有任意延遲(setTimeout、sleep、time.sleep())
- 測試不穩定(有時通過、負載下失敗)
- 並行執行時測試逾時
- 等待非同步操作完成
Don't use when: - Testing actual timing behavior (debounce, throttle intervals) - Always document WHY if using arbitrary timeout
不適用時機: - 測試真正的時序行為(debounce、throttle 間隔) - 若使用任意逾時,一定要說明「為什麼」
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for count | waitFor(() => items.length >= 5) |
| Wait for file | waitFor(() => fs.existsSync(path)) |
| Complex condition | waitFor(() => obj.ready && obj.value > 10) |
| 情境 | 模式 |
|---|---|
| 等待事件 | waitFor(() => events.find(e => e.type === 'DONE')) |
| 等待狀態 | waitFor(() => machine.state === 'ready') |
| 等待數量 | waitFor(() => items.length >= 5) |
| 等待檔案 | waitFor(() => fs.existsSync(path)) |
| 複雜條件 | waitFor(() => obj.ready && obj.value > 10) |
Generic polling function:
```typescript
async function waitFor
泛用輪詢函式:
```typescript
async function waitFor
while (true) { const result = condition(); if (result) return result;
while (true) { const result = condition(); if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
} } ```
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
} } ```
See condition-based-waiting-example.ts in this directory for complete implementation with domain-specific helpers (waitForEvent, waitForEventCount, waitForEventMatch) from actual debugging session.
本目錄中的 condition-based-waiting-example.ts 提供完整實作,內含來自實際除錯 session 的領域特定輔助函式(waitForEvent、waitForEventCount、waitForEventMatch)。
❌ Polling too fast: setTimeout(check, 1) - wastes CPU
✅ Fix: Poll every 10ms
❌ 輪詢太快: setTimeout(check, 1) —— 浪費 CPU
✅ 修正: 每 10ms 輪詢一次
❌ No timeout: Loop forever if condition never met ✅ Fix: Always include timeout with clear error
❌ 沒有逾時: 條件永遠不成立時會無限迴圈 ✅ 修正: 永遠要帶逾時,並附上清楚的錯誤
❌ Stale data: Cache state before loop ✅ Fix: Call getter inside loop for fresh data
❌ 資料過期: 在迴圈前就快取狀態 ✅ 修正: 在迴圈內呼叫 getter 取得最新資料
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justified
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justified
Requirements: 1. First wait for triggering condition 2. Based on known timing (not guessing) 3. Comment explaining WHY
條件: 1. 先等待觸發條件 2. 基於已知的時序(不是猜測) 3. 加上註解說明「為什麼」
From debugging session (2025-10-03): - Fixed 15 flaky tests across 3 files - Pass rate: 60% → 100% - Execution time: 40% faster - No more race conditions
來自除錯 session(2025-10-03): - 修好 3 個檔案中 15 個不穩定的測試 - 通過率:60% → 100% - 執行時間:快了 40% - 不再有競態條件