name: 条件等待 description: 通过用条件轮询替换任意超时来修复不稳定测试。当测试间歇性失败、有setTimeout延迟或涉及需要正确等待条件的异步操作时使用。
条件等待
配合使用: writing-tests 技能进行整体测试指导。本技能专注于基于时间的不稳定性问题。
相关: 如果测试单独通过但并发时失败,问题可能是共享状态而不是时间。参见 fixing-flaky-tests 技能进行诊断。
概述
不稳定测试通常通过任意延迟猜测时间。这会产生竞态条件,导致测试在快速机器上通过但在负载下或CI中失败。
核心原则: 等待你关心的实际条件,而不是猜测它需要多长时间。
使用时机
测试有任意延迟(setTimeout/sleep)吗?
│
├─ 测试实际时间行为(去抖动、节流)吗?
│ └─ 是 → 保留超时,记录原因
│
└─ 否 → 替换为条件等待
何时使用:
- 测试有任意延迟(
setTimeout、sleep、time.sleep()) - 测试因时间相关错误而不稳定
- 等待异步操作完成
何时不使用:
- 测试实际时间行为(去抖动、节流、间隔)
- 问题是测试之间的共享状态(使用
fixing-flaky-tests)
核心模式
// 坏:猜测时间
await new Promise((r) => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// 好:等待条件(返回结果)
const result = await waitFor(() => getResult(), '结果可用');
expect(result).toBeDefined();
实现
优先使用框架内置功能,如果可用:
- Testing Library:
findBy查询、waitFor - Playwright: 自动等待、
expect(locator).toBeVisible() - pytest:
asyncio.wait_for、tenacity
自定义轮询回退 当内置功能不足时:
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(`在 ${timeoutMs}ms 后等待 ${description} 超时`);
}
await new Promise((r) => setTimeout(r, 50)); // 轮询间隔
}
}
常见用例:
waitFor(() => events.find(e => e.type === 'DONE'), '完成事件')waitFor(() => machine.state === 'ready', '准备状态')waitFor(() => items.length >= 5, '5+ 项目')
语言特定模式
| 栈 | 参考 |
|---|---|
| Python (pytest, asyncio, tenacity) | references/python.md |
| TypeScript (Jest, Testing Library, Playwright) | references/typescript.md |
常见错误
| 错误 | 问题 | 修复 |
|---|---|---|
| 轮询太快 | setTimeout(check, 1) 浪费CPU |
每50ms轮询一次 |
| 无超时 | 如果条件未满足则无限循环 | 始终包括超时 |
| 陈旧数据 | 循环前缓存状态 | 在循环内调用获取器 |
| 无描述 | “超时”无上下文 | 包括你等待的内容 |
何时任意超时是正确的
// 工具每100ms打勾 - 需要2个打勾以验证部分输出
await waitForEvent(manager, "TOOL_STARTED"); // 首先:等待触发条件
await new Promise((r) => setTimeout(r, 200)); // 然后:等待定时行为
// 200ms = 在100ms间隔下的2个打勾 - 记录并说明原因
要求:
- 首先等待触发条件
- 基于已知时间(非猜测)
- 注释解释原因