概述
不稳定的测试(flaky tests)常常用任意延迟来猜测时序,这会产生竞态条件:测试在快速机器上通过,但在负载下或 CI 中失败。
核心原则:等待你真正关心的实际条件,而不是猜测它需要多长时间。
何时使用
- 测试中存在任意延迟(
setTimeout、sleep、time.sleep()) - 测试不稳定(有时通过,在负载下失败)
- 并行运行时测试超时
- 等待异步操作完成
不要用于:
- 测试真实的时序行为(防抖、节流间隔)
- 如果使用任意超时,始终记录原因
核心模式
// ❌ 之前:猜测时序
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ 之后:等待条件
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
快速模式
- 等待事件:
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)
实现
通用轮询函数:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
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
}
}
参见 @example.ts 获取完整实现,其中包含来自实际调试会话的领域特定辅助函数(waitForEvent、waitForEventCount、waitForEventMatch)。
常见错误
- ❌ 轮询过快:
setTimeout(check, 1)—— 浪费 CPU- ✅ 修复:每 10ms 轮询一次
- ❌ 没有超时:如果条件永远不满足,循环将永远进行
- ✅ 修复:始终包含超时并给出清晰的错误
- ❌ 数据陈旧:在循环前缓存状态
- ✅ 修复:在循环内调用 getter 以获取最新数据
何时任意超时是正确的
// 工具每 100ms tick 一次——需要 2 个 tick 来验证部分输出
await waitForEvent(manager, 'TOOL_STARTED'); // 首先:等待条件
await new Promise(r => setTimeout(r, 200)); // 然后:等待定时行为
// 200ms = 在 100ms 间隔下的 2 个 tick——已记录且合理
要求:
- 首先等待触发条件
- 基于已知时序(而非猜测)
- 注释解释原因
实际影响
来自调试会话(2025-10-03):
- 修复了 3 个文件中的 15 个不稳定测试
- 通过率:60% → 100%
- 执行时间:快 40%
- 不再有竞态条件