条件等待 condition-based-waiting

条件等待是一种软件测试方法,用于修复不稳定的测试,通过用条件轮询替代任意超时,确保在异步操作中等待实际条件而不是依赖时间猜测。它提高测试的稳定性和可靠性,适用于解决时间相关错误、间歇性失败和异步等待问题。关键词包括:条件等待、测试、不稳定性、超时、轮询、异步操作、竞态条件。

测试 0 次安装 0 次浏览 更新于 3/5/2026

name: 条件等待 description: 通过用条件轮询替换任意超时来修复不稳定测试。当测试间歇性失败、有setTimeout延迟或涉及需要正确等待条件的异步操作时使用。

条件等待

配合使用: writing-tests 技能进行整体测试指导。本技能专注于基于时间的不稳定性问题。

相关: 如果测试单独通过但并发时失败,问题可能是共享状态而不是时间。参见 fixing-flaky-tests 技能进行诊断。

概述

不稳定测试通常通过任意延迟猜测时间。这会产生竞态条件,导致测试在快速机器上通过但在负载下或CI中失败。

核心原则: 等待你关心的实际条件,而不是猜测它需要多长时间。

使用时机

测试有任意延迟(setTimeout/sleep)吗?
    │
    ├─ 测试实际时间行为(去抖动、节流)吗?
    │   └─ 是 → 保留超时,记录原因
    │
    └─ 否 → 替换为条件等待

何时使用:

  • 测试有任意延迟(setTimeoutsleeptime.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个打勾 - 记录并说明原因

要求:

  1. 首先等待触发条件
  2. 基于已知时间(非猜测)
  3. 注释解释原因