Sinon fake timers 之 `clock.runMicrotasks()`:手动刷新微任务队列的原理与实战
2026/9/24 17:18:54 网站建设 项目流程
  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

本篇技术指南围绕 Sinon.JS fake timers 提供的clock.runMicrotasks()方法展开,讲解它如何在不推进时钟的前提下同步执行process.nextTickqueueMicrotaskPromise回调等微任务,并结合仓库内 run-microtasks.test.js 测试用例与 fake-timers.js 源码剖析其底层机制。读完本文,你将掌握 fake timers 环境下微任务与宏任务(timer)的执行边界、runMicrotasks()tick()/runAll()的配合方式,以及在异步测试中避免回调悬挂的实战技巧。

一、clock.runMicrotasks()是什么

在 Sinon.JS 的 fake timers 体系中,clock对象是对真实全局定时器(setTimeoutsetIntervalDateTemporal等)的同步替身,用于在测试中精确控制时间。而clock.runMicrotasks()负责的是另一条独立的执行线——微任务队列(microtask queue)

根据 run-microtasks.md 的定义:

Runs all pending microtasks (e.g.process.nextTickorPromisecallbacks).

即:运行所有尚未执行的微任务,典型代表是process.nextTick回调与Promise回调(浏览器与 Node.js 中还包括queueMicrotask)。这一调用是同步的,调用结束后微任务队列被清空,且不会推进时钟的时间

二、为什么需要手动刷新微任务

JavaScript 事件循环将任务分为宏任务(macrotask,如setTimeoutsetIntervalsetImmediate)与微任务(microtask,如Promise.thenqueueMicrotask、Node.js 的process.nextTick)。在真实环境中,微任务由事件循环在每轮宏任务结束后自动清空;但 fake timers 把定时器换成了同步实现,时间由开发者手动拨动,此时事件循环的自动微任务调度并不会同步发生

典型场景:

const clock = sinon.useFakeTimers(); Promise.resolve().then(() => { console.log("promise resolved"); }); console.log("sync code"); // 此时 Promise 回调尚未执行 —— 微任务不会自动刷新

在 fake timers 控制下的同步测试里,若业务代码依赖Promiseprocess.nextTick的延续逻辑,就必须显式调用clock.runMicrotasks()来让这些回调落地,否则会出现"断言永远不满足"的悬挂问题。

三、基本行为与使用方式

clock.runMicrotasks()sinon.useFakeTimers()返回的clock对象提供,使用方式极为简单:

import sinon from "sinon"; const clock = sinon.useFakeTimers(); let called = false; Promise.resolve().then(() => { called = true; }); // 此刻 called === false clock.runMicrotasks(); // 此刻 called === true,微任务已被同步刷新 clock.restore();

三个可验证的核心行为

仓库中的测试文件 run-microtasks.test.js 用三个独立的 tap 测试精确锁定了该方法的行为边界:

  1. 刷新process.nextTick且不推进时间
const clock = sinon.useFakeTimers({ toFake: ["nextTick"] }); let called = false; process.nextTick(() => { called = true; }); t.notOk(called, "nextTick callback should not have run yet"); clock.runMicrotasks(); t.ok(called, "nextTick callback should have run"); t.equal(clock.now, 0, "clock time should not have advanced");

注意最后一行断言:clock.now仍为初始值0。这说明runMicrotasks()只清空微任务队列,不拨动时钟,与tick()的行为有本质区别。

  1. 刷新queueMicrotask回调
const clock = sinon.useFakeTimers({ toFake: ["queueMicrotask"] }); let called = false; queueMicrotask(() => { called = true; }); clock.runMicrotasks(); t.ok(called, "queueMicrotask callback should have run");
  1. 不会触发setTimeout这类宏任务
const clock = sinon.useFakeTimers(); let timerCalled = false; setTimeout(() => { timerCalled = true; }, 0); clock.runMicrotasks(); t.notOk(timerCalled, "setTimeout callback should not have fired");

这是最重要的边界:runMicrotasks()只处理微任务,绝不触碰定时器。即便setTimeout(..., 0)已经到期排队,也不会因为调用runMicrotasks()而执行。

关于toFake配置的说明

上述测试分别使用了{ toFake: ["nextTick"] }{ toFake: ["queueMicrotask"] }。需要澄清:这些配置影响的是哪些全局函数被替换成假实现,而runMicrotasks()方法本身始终可用——它的职责是刷新当前 fake timers 上下文中的微任务队列,与toFake的取值无直接依赖关系。依据 use-fake-timers.md 中的配置表,默认情况下 fake timers 会替换除process.nextTick之外的所有可替换方法;如需将nextTick也纳入替换范围,可显式传入toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "setImmediate", "clearImmediate", "Date", "nextTick", "hrtime", "performance"]

四、与tick()/runAll()的定位差异

理解runMicrotasks()最有效的方式,是与 fake timers 家族的其他时间推进方法对照。下表汇总了仓库中相关文档的核心定位:

方法核心行为是否推进时钟是否执行定时器文档
clock.runMicrotasks()刷新全部待处理微任务(Promise / nextTick / queueMicrotask)run-microtasks.md
clock.tick(time)将时钟向前推进time毫秒,触发该时间范围内到期的定时器tick.md
clock.next()推进到下一个已调度定时器的时刻并触发它是(仅下一个)next.md
clock.runAll()运行所有待处理定时器直到清空(含执行期间新添加的)run-all.md
clock.runToLast()运行到最后一个已调度定时器被触发run-to-last.md

从源码结构看(src/sinon/util/fake-timers.js),clock对象实际是@sinonjs/fake-timersinstall()的产物,因此runMicrotasks()的实现来自上游 fake-timers 库;Sinon 的useFakeTimers()只是负责组装配置并把它安装到全局对象上。

异步变体与微任务的交互

tick 系列还提供了异步变体tickAsync()runAllAsync()nextAsync()runToLastAsync()。文档明确指出这些异步版本会中断事件循环,让已调度的 Promise 回调在运行定时器之前执行

ThetickAsync()will also break the event loop, allowing any scheduled promise callbacks to executebeforerunning the timers.

也就是说,当你使用异步变体推进时间时,Promise 微任务会被自动放行;而runMicrotasks()的价值恰恰在于同步场景——不需要async/await、不需要中断事件循环,就能在当前调用栈内一次性清空微任务,适合纯同步断言风格的测试。

五、源码层面的佐证

在 fake-timers.js 中可以看到 Sinon 如何把 fake timers 组装起来:

  • useFakeTimers(dateOrConfig)(L40-L72)支持三种调用形态:无参数(now: 0起步)、传 epoch 数字或Date、传配置对象;配置对象中的global字段会被拆出用于指定目标全局上下文(如 JSDOM 的 window)。
  • createClock(config, globalCtx)(L14-L22)在给定全局上下文下调用FakeTimers.install(config)生成 clock,并把restore别名到uninstall
  • 文件末尾的timers对象(L80-L97)通过addIfDefined按环境能力收集可替换的全局函数,其中就包括nextTickqueueMicrotask——这正是runMicrotasks()要刷新的两类微任务来源。

由此可以确认:Sinon 的 fake timers 本身就是@sinonjs/fake-timers(原 lolex)的薄封装,runMicrotasks()的行为由上游库保证,Sinon 侧无需额外实现,测试直接通过sinon.useFakeTimers()的返回值调用即可。

六、实战场景一:同步测试 Promise 链

许多被测试函数会先执行同步逻辑,再通过Promise.resolve().then()或 async 函数延续。在 fake timers 下,同步断言常常会"抢先"于微任务执行:

import sinon from "sinon"; // 被测代码 function loadUser() { return Promise.resolve().then(() => ({ id: 1, name: "Ada" })); } // 测试 const clock = sinon.useFakeTimers(); let user = null; loadUser().then((u) => { user = u; }); // 断言前先刷新微任务 clock.runMicrotasks(); if (user !== null) { console.log(user.name); // "Ada" } clock.restore();

如果不调用runMicrotasks()user会一直是null,测试就会产生误判。

七、实战场景二:Node.js 的process.nextTick与事件循环惯用法

Node.js 生态中大量库依赖process.nextTick实现"当前阶段末尾执行"。结合 run-microtasks.test.js 第一个用例的写法,可以用{ toFake: ["nextTick"] }精确替换该函数,再用runMicrotasks()驱动:

const clock = sinon.useFakeTimers({ toFake: ["nextTick"] }); const events = []; process.nextTick(() => events.push("nextTick")); clock.runMicrotasks(); console.log(events); // ["nextTick"] clock.restore();

同理,浏览器端或 Web Worker 环境中的queueMicrotask也可以用相同模式测试(第二个用例即验证此场景)。

八、实战场景三:在 Sandbox 中组合使用

在真实测试套件中,更推荐把 fake timers 挂到 sandbox 上,借助 sandboxes/api/use-fake-timers.md 描述的sinon.createSandbox({ useFakeTimers: true })统一管理安装与恢复:

import sinon from "sinon"; const sandbox = sinon.createSandbox({ useFakeTimers: true }); let resolved = false; Promise.resolve().then(() => { resolved = true; }); sandbox.clock.runMicrotasks(); console.log(resolved); // true sandbox.restore(); // 统一恢复被替换的全局函数

sandbox.clockuseFakeTimers()返回的 clock 行为一致,runMicrotasks()均可直接调用。

九、注意事项与最佳实践

  • 只刷微任务,不碰定时器:若测试同时存在setTimeout与 Promise 回调,runMicrotasks()只处理后者;定时器仍需tick()/runAll()驱动。
  • 不推进时间clock.now在调用前后保持不变,因此它不会影响任何与绝对时间相关的断言。
  • 同步执行runMicrotasks()不是 async 方法,调用返回时微任务队列已清空,无需await
  • 与 async 变体的取舍:如果测试本身是 async 函数,且需要"先跑 Promise 回调再跑定时器"的顺序,可直接使用tickAsync()/runAllAsync();而纯同步测试优先选择runMicrotasks(),代码更简洁、执行路径更可控。
  • 记得 restore:无论通过clock.restore()还是sandbox.restore(),都要在测试收尾时恢复被替换的全局函数,避免污染其他用例(参考 sandboxes/api/restore.md)。

十、相关 API 导航

  • fake timers 安装与配置:use-fake-timers.md
  • 时间推进:tick.md、next.md、run-all.md、run-to-last.md
  • 时钟管理:reset.md、restore.md、set-system-time.md
  • 概念总览:fake-timers/index.md
  • 异步测试进阶:fake-timers-async.md
  • 实现与测试:fake-timers.js、run-microtasks.test.js

小结

clock.runMicrotasks()是 Sinon fake timers 中容易被忽略却非常关键的方法:它把"微任务刷新"从事件循环的自动行为中解放出来,让开发者在同步测试中精确控制Promise/process.nextTick/queueMicrotask回调的执行时机,且不与时钟推进耦合。配合 run-microtasks.test.js 中锁定的三条行为边界(刷新 nextTick、刷新 queueMicrotask、不触发 setTimeout),你可以放心地在自己的测试代码中组合使用它与tick()/runAll(),彻底掌控 fake timers 下的任务调度顺序。

  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

相关推荐

上一篇:NVIDIA-Nemotron-3-Nano-4B-GGUF性能测试:在Jetson Thor上的惊人表现 🚀
下一篇:AATree 深度解析:iTerm2 中的 Arne Andersson 平衡二叉搜索树(NSMutableDictionary 子类)实现与应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询