You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Sinon Fake Timer测试中多次调用仅触发一次问题排查

问题分析与解决方案

我来帮你排查下这个问题,核心其实有两个关键问题:你的被测函数存在Promise未正确等待异步操作的逻辑缺陷,还有一个隐藏的变量与类型名混淆的bug,我们一步步来解决:

1. 修复被测函数的核心问题

你的awaitQueryCompletion函数虽然标记为async,但内部的setTimeout回调并没有被包裹进Promise中——这意味着函数会直接返回一个已resolve的Promise,根本不会等待setTimeout里的异步逻辑执行完成,递归调用的await自然也不会生效。

同时你代码里还犯了一个低级错误:用类型名ExecutionOutput去访问属性,而不是你声明的变量output,这会导致state永远是undefined,递归逻辑根本不会触发。

修复后的被测函数如下:

private async awaitQueryCompletion(queryId: string): Promise<void> {
  // 把setTimeout包装成Promise,确保函数真正等待异步回调执行完成
  await new Promise<void>((resolve) => {
    setTimeout(async () => {
      const output: ExecutionOutput = await this.getQueryExecution({ QueryExecutionId: queryId }).promise();
      let state: string | undefined;
      // 修复:用变量output而非类型名ExecutionOutput访问属性
      if (output.QueryExecution !== undefined && output.QueryExecution.Status) {
        state = output.QueryExecution.Status.State;
      }
      if (state !== undefined && state === "RUNNING") {
        await this.awaitQueryCompletion(queryId);
      }
      // 回调执行完毕后,resolve外层Promise
      resolve();
    }, this.RETRY_INTERVAL);
  });
}

2. 调整测试代码的执行逻辑

修复函数后,测试代码需要确保在推进计时器后,给事件循环足够时间处理异步任务,同时要等待整个异步流程完成。这里提供两种可行的测试写法:

写法1:分步推进计时器并等待任务处理

it.only("Should call getQueryExecution twice with correct params", async () => {
  const INTERVAL: number = 5010;
  getQueryExecutionStub.onFirstCall()
    .returns({ promise: async (): Promise<ExecutionOutput> => {
      return Promise.resolve({ QueryExecution: { Status: { State: "RUNNING" } } });
    } });
  getQueryExecutionStub.onSecondCall()
    .returns({promise: async (): Promise<ExecutionOutput> => {
      return Promise.resolve({ QueryExecution: { Status: { State: "SUCCEEDED" } } });
    } });

  // 启动异步流程但暂不等待
  const queryPromise = selector.query(testInput);
  
  // 第一次推进时间,触发第一个setTimeout
  timer.tick(INTERVAL);
  // 等待事件循环处理当前异步任务(第一次getQueryExecution调用和递归逻辑)
  await Promise.resolve();
  
  // 第二次推进时间,触发递归里的setTimeout
  timer.tick(INTERVAL);
  await Promise.resolve();
  
  // 等待整个query流程完全结束
  await queryPromise;

  expect(getQueryExecutionStub.calledTwice).to.equal(true);
});

写法2:一次性触发所有pending计时器(更简洁)

it.only("Should call getQueryExecution twice with correct params", async () => {
  getQueryExecutionStub.onFirstCall()
    .returns({ promise: async (): Promise<ExecutionOutput> => {
      return Promise.resolve({ QueryExecution: { Status: { State: "RUNNING" } } });
    } });
  getQueryExecutionStub.onSecondCall()
    .returns({promise: async (): Promise<ExecutionOutput> => {
      return Promise.resolve({ QueryExecution: { Status: { State: "SUCCEEDED" } } });
    } });

  const queryPromise = selector.query(testInput);
  // 一次性触发所有pending的计时器,包括递归产生的setTimeout
  timer.runAll();
  // 等待整个异步流程完成
  await queryPromise;

  expect(getQueryExecutionStub.calledTwice).to.equal(true);
});

关键总结

  • 异步递归函数必须确保每一层的异步操作都被Promise包裹,否则await无法正确等待后续逻辑。
  • 注意区分类型名和变量名,避免低级语法错误导致逻辑失效。
  • 使用Sinon Fake Timers时,要给事件循环留出处理异步任务的时间,要么分步tick后等待,要么用runAll一次性触发所有计时器。

内容的提问来源于stack exchange,提问作者Ethan Davis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 07:37:31