如何解决Sinon Stub setInterval调用的异步函数时的测试失败问题?
解决Mocha测试中setInterval触发异步函数的断言时机问题
问题场景
需要测试包含私有异步函数Bar的Foo类:Bar由setInterval定时触发,内部调用sleep异步函数,执行流程为A() → 等待sleep → B()。当前测试中,断言B()被调用时,实际B()尚未执行,导致测试失败——A()的输出先出现,测试断言失败后B()才执行。
待测试的Foo类代码
export class Foo { constructor () { setInterval(async () => await this.Bar(), 1000); } private async Bar() { this.A(); await this.sleep(5000); this.B(); } private A() { console.log("A"); } private B() { console.log("B"); } public async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } }
原测试代码(失败)
it.only("test", () => { let cut = new Foo(); let ASpy = sinon.spy(cut, 'A'); let BSpy = sinon.spy(cut, 'B'); sinon.stub(cut, 'sleep').resolves(); fakeClock.tick(1000); ASpy.should.be.calledOnce; BSpy.should.be.calledOnce; ASpy.restore(); BSpy.restore(); });
问题原因
测试函数为同步执行,fakeClock.tick(1000)触发setInterval回调后,Bar中的await sleep()(已被stub为立即resolve)会将this.B()的执行放入微任务队列,而同步断言代码会在微任务执行前运行,此时B()还未被调用,导致断言失败。
解决方案
1. 将测试改为异步函数,等待微任务执行
修改测试函数为异步函数,在fakeClock.tick()后等待微任务队列清空,确保B()执行完毕后再进行断言:
it.only("test", async () => { let cut = new Foo(); // TypeScript中私有方法需通过类型断言绕过检查 let ASpy = sinon.spy(cut as any, 'A'); let BSpy = sinon.spy(cut as any, 'B'); sinon.stub(cut, 'sleep').resolves(); fakeClock.tick(1000); // 等待微任务队列执行,确保this.B()被调用 await Promise.resolve(); ASpy.should.be.calledOnce; BSpy.should.be.calledOnce; ASpy.restore(); BSpy.restore(); });
2. 确保Fake Clock正确初始化
需在测试前后正确初始化和销毁Sinon的Fake Timer,覆盖setInterval和setTimeout:
let fakeClock; beforeEach(() => { fakeClock = sinon.useFakeTimers(); }); afterEach(() => { fakeClock.restore(); });
额外优化(可选)
TypeScript中直接Spy私有方法会有类型警告,若想避免可考虑:
- 将
A()、B()改为protected方法,测试时继承Foo类进行Spy - 重构代码,将
A()、B()的逻辑抽离为独立的可测试单元,通过依赖注入引入Foo类
内容的提问来源于stack exchange,提问作者wintlef
相关产品推荐
相关产品推荐

