如何用Sinon FakeTimers模拟延迟并完成精准计时单元测试?
解决Sinon FakeTimers测试冻结与精确时间断言问题
首先看你的测试代码核心问题:定时器操作顺序完全搞反了——你先推进了时间,再调用需要延迟的方法,这时候setTimeout还没被创建,fake timer的tick根本起不到作用,而await service.doSomething()会一直等待永远不会resolve的Promise,自然就卡住了。
问题1:修复测试冻结,正确推进时间
调整代码顺序,先触发异步方法让内部的setTimeout被fake timer接管,再推进时间,确保Promise能正确settle。这里有两种靠谱的写法:
写法1:先调用方法,再tick,最后await结果
describe('My Service', () => { it('should return done after 5s', async () => { const clock = sinon.useFakeTimers(0); const service = new MyService(); // 先触发方法,此时内部的setTimeout已被fake timer接管 const promise = service.doSomething(); // 推进时间到5000ms,触发setTimeout的回调,让Promise resolve clock.tick(5000); // 现在可以安全地获取结果 const ans = await promise; expect(ans).to.equal('Done'); // 还原真实定时器,避免影响其他测试 clock.restore(); }); });
写法2:结合microtask手动触发(兼容部分场景)
如果你的代码里有多层异步嵌套,可能需要在tick后手动触发microtasks,确保Promise完全settle:
describe('My Service', () => { it('should return done after 5s', async () => { const clock = sinon.useFakeTimers(0); const service = new MyService(); const promise = service.doSomething(); clock.tick(5000); // 手动触发microtask队列,确保Promise resolve await Promise.resolve(); const ans = await promise; expect(ans).to.equal('Done'); clock.restore(); }); });
问题2:断言精确5000ms延迟是否可行?
完全可行!有两种方式来验证:
方式1:检查setTimeout的调用参数
因为你的delay方法内部依赖setTimeout,我们可以全局spy这个方法,断言传入的延迟时间正好是5000ms:
describe('My Service', () => { it('should wait exactly 5000ms before returning', async () => { const clock = sinon.useFakeTimers(0); // 全局spy setTimeout const setTimeoutSpy = sinon.spy(global, 'setTimeout'); const service = new MyService(); const promise = service.doSomething(); clock.tick(5000); const ans = await promise; // 断言setTimeout被调用一次,且延迟参数是5000ms expect(setTimeoutSpy.calledOnce).to.be.true; expect(setTimeoutSpy.firstCall.args[1]).to.equal(5000); expect(ans).to.equal('Done'); setTimeoutSpy.restore(); clock.restore(); }); });
方式2:通过fake timer的时间戳验证
在tick前后读取fake timer的当前时间,确保时间差正好是5000ms:
describe('My Service', () => { it('should return exactly after 5000ms', async () => { const clock = sinon.useFakeTimers(0); const service = new MyService(); const startTime = clock.now; // 初始时间为0 const promise = service.doSomething(); clock.tick(5000); const endTime = clock.now; const ans = await promise; expect(endTime - startTime).to.equal(5000); expect(ans).to.equal('Done'); clock.restore(); }); });
额外注意事项
- 测试结束后必须调用
clock.restore(),否则fake timer会污染其他测试用例,导致奇怪的异步问题。 - 如果你使用Sinon v16+版本,
useFakeTimers的API有细微变化,比如需要传入{ shouldAdvanceTime: false }来固定初始时间,但核心逻辑保持一致。 - 如果你封装了自己的延迟工具类,可以直接spy这个工具类的方法,不用全局spy
setTimeout,更贴合业务逻辑。
内容的提问来源于stack exchange,提问作者TSR
相关产品推荐
相关产品推荐

