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

如何用Mocha和Chai覆盖Promise函数中if(timer)分支的测试

覆盖timeout函数未覆盖分支的Mocha+Chai测试方案

问题背景

现有一个TypeScript的timeout函数,已实现100%行覆盖率,但promise.then和promise.catch回调中的if(timer)分支的false路径未覆盖,需要编写单元测试补齐这部分分支覆盖率。

函数代码如下:

export function timeout<T>(promise: Promise<T>, millis: number, contextName?: string): Promise<T> {
  let contextPrefix = "Request";
  if (contextName) {
    contextPrefix = contextName;
  }
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => {
      clearTimeout(timer);
      logger.info(() => `${contextPrefix} timed out.`);
      reject(new Error(`${contextPrefix} timed out.`));
    }, millis);

    promise
      .then((response: T) => {
        if (timer) {
          clearTimeout(timer);
          resolve(response);
        }
      })
      .catch((err) => {
        const msg = (err as Error).message || (err as string);
        logger.warn(() => `${contextPrefix} error: ${msg}.`, {}, err as Error);
        if (timer) {
          clearTimeout(timer);
          reject(err instanceof Error ? err : new Error(msg));
        }
      });
  });
}

核心思路

要触发if(timer)的false分支,需要让timer变量的值为假值(如0、null),同时确保超时逻辑优先执行。最便捷的方式是通过mock全局定时器,让setTimeout返回假值并立即执行超时回调,这样当promise的回调触发时,if(timer)条件不成立。

我们使用sinon来mock定时器,配合mocha和chai完成测试。

测试代码实现

首先安装依赖:

npm install mocha chai sinon @types/mocha @types/chai @types/sinon --save-dev

测试用例实现

import { expect } from 'chai';
import sinon from 'sinon';
import { timeout } from './path-to-your-function';
import logger from './path-to-your-logger';

describe('timeout function', () => {
  // 每次测试后恢复所有mock
  afterEach(() => {
    sinon.restore();
  });

  // 覆盖then回调的if(timer) false分支
  it('should skip resolve logic when timer is falsy in then callback', async () => {
    // Mock setTimeout:立即执行超时回调,返回假值0作为timer
    const stubSetTimeout = sinon.stub(global, 'setTimeout').callsFake((cb) => {
      cb();
      return 0 as unknown as NodeJS.Timeout;
    });
    const stubClearTimeout = sinon.stub(global, 'clearTimeout');
    const loggerInfoStub = sinon.stub(logger, 'info');

    // 创建立即resolve的promise
    const mockPromise = Promise.resolve('test-data');
    const timeoutPromise = timeout(mockPromise, 100);

    await expect(timeoutPromise).to.be.rejectedWith('Request timed out.');
    expect(loggerInfoStub.calledOnce).to.be.true;
    // 验证仅超时回调中的clearTimeout被调用,then回调内的未执行
    expect(stubClearTimeout.calledOnce).to.be.true;
  });

  // 覆盖catch回调的if(timer) false分支
  it('should skip reject logic when timer is falsy in catch callback', async () => {
    const stubSetTimeout = sinon.stub(global, 'setTimeout').callsFake((cb) => {
      cb();
      return 0 as unknown as NodeJS.Timeout;
    });
    const stubClearTimeout = sinon.stub(global, 'clearTimeout');
    const loggerInfoStub = sinon.stub(logger, 'info');
    const loggerWarnStub = sinon.stub(logger, 'warn');

    // 创建立即reject的promise
    const mockPromise = Promise.reject(new Error('promise-failure'));
    const timeoutPromise = timeout(mockPromise, 100);

    await expect(timeoutPromise).to.be.rejectedWith('Request timed out.');
    expect(loggerInfoStub.calledOnce).to.be.true;
    // 验证catch回调中的warn日志未触发(if(timer)不成立)
    expect(loggerWarnStub.called).to.be.false;
    expect(stubClearTimeout.calledOnce).to.be.true;
  });
});

测试原理说明

  • 通过mocksetTimeout立即执行超时逻辑,让Promise提前进入rejected状态。
  • 让setTimeout返回假值0,使得then/catch回调中的if(timer)条件不成立,直接走false分支。
  • 验证日志和定时器方法的调用情况,确保分支逻辑被正确触发。

内容的提问来源于stack exchange,提问作者The Coder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 17:54:58