如何用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; }); });
测试原理说明
- 通过mock
setTimeout立即执行超时逻辑,让Promise提前进入rejected状态。 - 让
setTimeout返回假值0,使得then/catch回调中的if(timer)条件不成立,直接走false分支。 - 验证日志和定时器方法的调用情况,确保分支逻辑被正确触发。
内容的提问来源于stack exchange,提问作者The Coder
相关产品推荐
相关产品推荐

