SuperAPI AI 工具集

condition-based-waiting

测试与质量

查看项目

更新于 10-16 02:21

技能介绍

概述

不稳定的测试(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%
  • 不再有竞态条件