condition-based-waiting

systematic-debugging · 附屬文件

Condition-Based Waiting

條件式等待

Overview

概述

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.

核心原則: 等待你真正關心的條件,而不是猜測要花多久時間。

When to Use

使用時機

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

適用時機: - 測試含有任意延遲(setTimeoutsleeptime.sleep()) - 測試不穩定(有時通過、負載下失敗) - 並行執行時測試逾時 - 等待非同步操作完成

Don't use when: - Testing actual timing behavior (debounce, throttle intervals) - Always document WHY if using arbitrary timeout

不適用時機: - 測試真正的時序行為(debounce、throttle 間隔) - 若使用任意逾時,一定要說明「為什麼」

Core Pattern

核心模式

// ❌ 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();

Quick Patterns

快速模式

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)

Implementation

實作

Generic polling function: ```typescript async function waitFor( condition: () => T | undefined | null | false, description: string, timeoutMs = 5000 ): Promise { const startTime = Date.now();

泛用輪詢函式: ```typescript async function waitFor( condition: () => T | undefined | null | false, description: string, timeoutMs = 5000 ): Promise { const startTime = Date.now();

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 的領域特定輔助函式(waitForEventwaitForEventCountwaitForEventMatch)。

Common Mistakes

常見錯誤

❌ 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 取得最新資料

When Arbitrary Timeout IS Correct

何時「任意逾時」是正確的

// 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. 加上註解說明「為什麼」

Real-World Impact

實際影響

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% - 不再有競態條件