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
相关产品推荐
相关产品推荐

