ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Jest Timer Mocks 完全指南:用 Jest 假定时器精确控制测试中的时间流逝

2026/9/19 19:12:44 拓冰建站 浏览量
Jest Timer Mocks 完全指南:用 Jest 假定时器精确控制测试中的时间流逝 Jest Timer Mocks 完全指南用 Jest 假定时器精确控制测试中的时间流逝【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest本指南基于 Jest 官方文档website/versioned_docs/version-30.0/TimerMocks.md编写全面讲解如何用jest.useFakeTimers()替换setTimeout()、setInterval()等原生定时器并通过runAllTimers()、advanceTimersByTime()、runOnlyPendingTimers()等时间控制 API 在测试中快进时间。读完本文你将掌握为依赖定时器的代码编写确定、快速、无竞态测试的完整方案包括递归定时器、动画帧与选择性伪造等进阶场景。为什么需要假定时器在测试环境中setTimeout()、setInterval()、clearTimeout()、clearInterval()这些原生定时器函数并不理想因为它们依赖真实时间的流逝——一个等待 1 秒的测试真的要等 1 秒一个 10 分钟的超时逻辑则根本无法在测试里等完。Jest 的解决方案是用假定时器Fake Timers替换原生定时器让时间变成你可以手动拨动的时钟。从实现上看Jest 的现代假定时器实现位于 packages/jest-fake-timers/src/modernFakeTimers.ts底层基于sinonjs/fake-timers。启用后Date、performance.now()、queueMicrotask()、setImmediate()、clearImmediate()、setInterval()、clearInterval()、setTimeout()、clearTimeout()都会被替换为从假时钟获取时间的实现在 Node 环境中还会替换process.hrtime、process.nextTick()在 jsdom 环境中则额外替换requestAnimationFrame()、cancelAnimationFrame()、requestIdleCallback()、cancelIdleCallback()。启用假定时器useFakeTimers 与 useRealTimers启用假定时器只需调用jest.useFakeTimers()它会替换setTimeout()及其他定时器函数的原始实现调用jest.useRealTimers()即可恢复原生行为。jest.useFakeTimers()会影响当前测试文件内所有测试直到调用jest.useRealTimers()恢复。以下是一个经典例子——一个 1 秒后触发回调的游戏计时模块function timerGame(callback) { console.log(Ready....go!); setTimeout(() { console.log(Times up -- stop!); callback callback(); }, 1000); } module.exports timerGame;对应的测试断言游戏确实只调度了一次 1 秒的定时器jest.useFakeTimers(); jest.spyOn(global, setTimeout); test(waits 1 second before ending the game, () { const timerGame require(../timerGame); timerGame(); expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); });注意这里用了jest.spyOn(global, setTimeout)来监听定时器调用由于假定时器替换了全局的setTimeout通过 spy 就能断言它被调用的次数与参数。jest.useFakeTimers()与jest.useRealTimers()可以在文件顶层、test块内部等任何位置调用但它们都是全局操作会影响同一文件中的其他测试。在同一个测试文件中再次调用jest.useFakeTimers()会重置内部状态如定时器计数并按传入的新配置重新安装假定时器。运行全部定时器jest.runAllTimers()要断言回调在 1 秒后被调用无需等待真实时间可以在测试中途用 Jest 的定时器控制 API 快进时间jest.useFakeTimers(); test(calls the callback after 1 second, () { const timerGame require(../timerGame); const callback jest.fn(); timerGame(callback); // At this point in time, the callback should not have been called yet expect(callback).not.toHaveBeenCalled(); // Fast-forward until all timers have been executed jest.runAllTimers(); // Now our callback should have been called! expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(1); });jest.runAllTimers()会同时耗尽宏任务队列由setTimeout()、setInterval()、setImmediate()排队的任务和微任务队列通常由process.nextTick()排队。如果这些任务自身又调度了新任务会持续执行直到队列清空。它非常适合在测试中同步执行 setTimeout 回调从而同步断言只有回调执行后才会发生的状态。从源码看runAllTimers()最终调用this._clock.runAll()见 packages/jest-fake-timers/src/modernFakeTimers.ts即直接执行假时钟的runAll方法。只运行待处理定时器jest.runOnlyPendingTimers()有些场景存在递归定时器——定时器的回调里又设置新的定时器。对这类代码执行runAllTimers()会陷入无限循环并抛出错误Aborting after running 100000 timers, assuming an infinite loop!。此时应使用jest.runOnlyPendingTimers()它只执行当前已排队的宏任务回调中新调度的定时器不会在这次调用中被执行。function infiniteTimerGame(callback) { console.log(Ready....go!); setTimeout(() { console.log(Times up! 10 seconds before the next game starts...); callback callback(); // Schedule the next game in 10 seconds setTimeout(() { infiniteTimerGame(callback); }, 10000); }, 1000); } module.exports infiniteTimerGame;对应测试逐步推进时间验证1 秒定时器触发回调并新建一个 10 秒定时器jest.useFakeTimers(); jest.spyOn(global, setTimeout); describe(infiniteTimerGame, () { test(schedules a 10-second timer after 1 second, () { const infiniteTimerGame require(../infiniteTimerGame); const callback jest.fn(); infiniteTimerGame(callback); // At this point in time, there should have been a single call to // setTimeout to schedule the end of the game in 1 second. expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); // Fast forward and exhaust only currently pending timers // (but not any new timers that get created during that process) jest.runOnlyPendingTimers(); // At this point, our 1-second timer should have fired its callback expect(callback).toHaveBeenCalled(); // And it should have created a new timer to start the game over in // 10 seconds expect(setTimeout).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); }); });runOnlyPendingTimers()在源码中对应this._clock.runToLast()见 modernFakeTimers.ts即推进到最后一个已排队的定时器。调节递归定时器上限timerLimit在抛出错误前最多运行的定时器数量也是可配置的这在调试或特殊场景下很有用jest.useFakeTimers({timerLimit: 100});timerLimit的默认值是100_000。从源码看它被映射为 sinon fake timers 的loopLimit配置见 modernFakeTimers.ts源码注释也说明最大运行 100000 个定时器后判定为无限循环并中止。按毫秒推进时间jest.advanceTimersByTime()另一个常用 API 是jest.advanceTimersByTime(msToRun)。调用时所有定时器都会向前推进msToRun毫秒凡是经由setTimeout()或setInterval()排队、且会在该时间段内执行的待处理宏任务都会被执行此外如果这些宏任务又调度了同一时间段内该执行的新宏任务它们也会被持续执行直到队列中没有剩余需要在msToRun毫秒内运行的宏任务为止。function timerGame(callback) { console.log(Ready....go!); setTimeout(() { console.log(Times up -- stop!); callback callback(); }, 1000); } module.exports timerGame;jest.useFakeTimers(); it(calls the callback after 1 second via advanceTimersByTime, () { const timerGame require(../timerGame); const callback jest.fn(); timerGame(callback); // At this point in time, the callback should not have been called yet expect(callback).not.toHaveBeenCalled(); // Fast-forward until all timers have been executed jest.advanceTimersByTime(1000); // Now our callback should have been called! expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(1); });advanceTimersByTime(msToRun)在源码中最终调用假时钟的tick()方法见 modernFakeTimers.ts。需要注意的是它只执行宏任务队列setTimeout()、setInterval()及setImmediate()排队的任务因此适合精确控制推进多少毫秒而非推完所有任务。此外有时在测试中清空所有待处理定时器也很必要可以使用jest.clearAllTimers()它会从定时器系统中移除所有待处理定时器已调度但尚未执行的定时器将被清除、永远不再有机会执行。推进到下一动画帧jest.advanceTimersToNextFrame()在动画类应用中经常通过requestAnimationFrame调度工作。Jest 提供了便捷方法jest.advanceTimersToNextFrame()用于把时间推进到足以执行所有当前已调度的动画帧回调。在模拟计时中动画帧在时钟启动后每16ms执行一次对应大约每秒 60 帧。当你用requestAnimationFrame(callback)调度回调后时钟前进16ms时回调会被调用。advanceTimersToNextFrame()会把时钟精确推进到下一个16ms增量例如某动画帧回调调度后时钟已经过了6ms那么该方法会把时钟再推进10ms。jest.useFakeTimers(); it(calls the animation frame callback after advanceTimersToNextFrame(), () { const callback jest.fn(); requestAnimationFrame(callback); // At this point in time, the callback should not have been called yet expect(callback).not.toHaveBeenCalled(); jest.advanceTimersToNextFrame(); // Now our callback should have been called! expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalledTimes(1); });源码实现中advanceTimersToNextFrame()调用this._clock.runToFrame()见 modernFakeTimers.ts。在 packages/jest-fake-timers/src/tests/modernFakeTimers.test.ts 的advanceTimersToNextFrame测试组中覆盖了按顺序执行多个动画帧回调一次只执行一帧已取消的动画帧不执行与 setTimeout 混合调度时按时间先后执行等行为例如同时存在 10ms 的setTimeout与动画帧回调时advanceTimersToNextFrame()会先执行超时任务再执行帧回调。选择性伪造doNotFake有些情况下你的代码可能不希望某个或某几个 API 的原始实现被覆盖。此时可以使用doNotFake选项。例如在 jsdom 环境中为performance.mark()提供自定义 mock/** * jest-environment jsdom */ const mockPerformanceMark jest.fn(); window.performance.mark mockPerformanceMark; test(allows mocking performance.mark(), () { jest.useFakeTimers({doNotFake: [performance]}); expect(window.performance.mark).toBe(mockPerformanceMark); });doNotFake接受一个 API 名称数组可选的名称包括Date、hrtime、nextTick、performance、queueMicrotask、requestAnimationFrame、cancelAnimationFrame、requestIdleCallback、cancelIdleCallback、setImmediate、clearImmediate、setInterval、clearInterval、setTimeout、clearTimeout、Temporal默认值为[]即伪造全部 API。源码中doNotFake的实现是从待伪造集合中逐个删除对应 API 名见 modernFakeTimers.ts。在 e2e 测试 e2e/fake-timers/do-not-fake/tests/doNotFake.test.js 中验证了默认情况下globalThis.performance.mark会被替换而传入{doNotFake: [performance]}后它保持为自定义 mock。更多配置与相关 API 速查useFakeTimers 完整配置项jest.useFakeTimers(fakeTimersConfig?)支持以下配置类型定义见 docs/JestObjectAPI.md配置项类型默认值说明advanceTimersboolean \| numberfalse为true时所有定时器每 20 毫秒自动前进 20 毫秒传数字可自定义时间增量doNotFakeArrayFakeableAPI[]不应被伪造的 API 名称列表legacyFakeTimersbooleanfalse使用旧的假定时器实现不基于sinonjs/fake-timers该模式下不支持其余附加选项nownumber \| Date \| Temporal.Instant \| Temporal.ZonedDateTimeDate.now()设置假定时器使用的当前系统时间timerLimitnumber100_000调用jest.runAllTimers()时可运行的递归定时器最大数量例如在项目配置中为所有测试统一设置假定时器默认值const {defineConfig} require(jest); module.exports defineConfig({ fakeTimers: { doNotFake: [nextTick], timerLimit: 1000, }, });该fakeTimers配置项详见 docs/Configuration.md提供所有测试的默认假定时器配置jest.useFakeTimers()会使用这些默认值传入配置对象时则覆盖它们。相关定时器控制 API 速查除本文详述的 API 外docs/JestObjectAPI.md 还提供了以下配套方法可结合使用jest.runAllTicks()耗尽微任务队列通常经process.nextTick排队。jest.runAllTimersAsync()runAllTimers()的异步版本允许已调度的 Promise 回调先于定时器执行legacy 实现不可用。jest.advanceTimersByTimeAsync(msToRun)advanceTimersByTime()的异步版本msToRun也接受Temporal.Duration但不支持日历单位years、months、weeks使用时间单位days、hours、minutes、seconds、milliseconds会抛错。jest.runOnlyPendingTimersAsync()runOnlyPendingTimers()的异步版本。jest.advanceTimersToNextTimer(steps)只推进到下一个超时/间隔需要执行的毫秒数可传steps一次运行多个。jest.clearAllTimers()移除所有待处理定时器。jest.getTimerCount()返回仍未运行的假定时器数量。jest.now()返回当前假时钟的时间毫秒。jest.setSystemTime(now)模拟程序运行期间用户更改系统时钟它改变当前时间但本身不会触发定时器。jest.getRealSystemTime()在时间被伪造时获取真实当前时间。jest.setTimerTickMode(mode)配置假定时器的时间推进方式manual手动、nextAsync持续推进、interval等价于advanceTimers: true且默认 delta 为 20。这些异步版本 API如runAllTimersAsync、advanceTimersByTimeAsync仅在基于sinonjs/fake-timers的现代实现中可用legacy 假定时器不可用。总结Jest 假定时器把等待真实时间的测试痛点转化为可控、确定、毫秒级可验证的测试体验。核心工作流是jest.useFakeTimers()开启伪造 → 编写触发定时器的被测代码 → 用jest.runAllTimers()全部执行、jest.runOnlyPendingTimers()处理递归定时器、jest.advanceTimersByTime(ms)精确推进、jest.advanceTimersToNextFrame()驱动动画帧 → 最后用jest.useRealTimers()或jest.clearAllTimers()收尾。结合doNotFake、timerLimit、advanceTimers等配置项几乎可以覆盖真实项目中所有依赖时间的异步场景且无需任何真实等待。进一步阅读完整的 API 文档见 docs/JestObjectAPI.md配置项说明见 docs/Configuration.md实现源码见 packages/jest-fake-timers/src/modernFakeTimers.ts单元测试见 packages/jest-fake-timers/src/tests/modernFakeTimers.test.ts端到端用例见 e2e/fake-timers 目录。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考