- 测试
- 开发工具
【免费下载链接】sinon
Test spies, stubs and mocks for JavaScript.
本篇技术指南围绕 Sinon.JS fake timers 提供的clock.runMicrotasks()方法展开,讲解它如何在不推进时钟的前提下同步执行process.nextTick、queueMicrotask与Promise回调等微任务,并结合仓库内 run-microtasks.test.js 测试用例与 fake-timers.js 源码剖析其底层机制。读完本文,你将掌握 fake timers 环境下微任务与宏任务(timer)的执行边界、runMicrotasks()与tick()/runAll()的配合方式,以及在异步测试中避免回调悬挂的实战技巧。
一、clock.runMicrotasks()是什么
在 Sinon.JS 的 fake timers 体系中,clock对象是对真实全局定时器(setTimeout、setInterval、Date、Temporal等)的同步替身,用于在测试中精确控制时间。而clock.runMicrotasks()负责的是另一条独立的执行线——微任务队列(microtask queue)。
根据 run-microtasks.md 的定义:
Runs all pending microtasks (e.g.
process.nextTickorPromisecallbacks).
即:运行所有尚未执行的微任务,典型代表是process.nextTick回调与Promise回调(浏览器与 Node.js 中还包括queueMicrotask)。这一调用是同步的,调用结束后微任务队列被清空,且不会推进时钟的时间。
二、为什么需要手动刷新微任务
JavaScript 事件循环将任务分为宏任务(macrotask,如setTimeout、setInterval、setImmediate)与微任务(microtask,如Promise.then、queueMicrotask、Node.js 的process.nextTick)。在真实环境中,微任务由事件循环在每轮宏任务结束后自动清空;但 fake timers 把定时器换成了同步实现,时间由开发者手动拨动,此时事件循环的自动微任务调度并不会同步发生。
典型场景:
const clock = sinon.useFakeTimers(); Promise.resolve().then(() => { console.log("promise resolved"); }); console.log("sync code"); // 此时 Promise 回调尚未执行 —— 微任务不会自动刷新在 fake timers 控制下的同步测试里,若业务代码依赖Promise或process.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 测试精确锁定了该方法的行为边界:
- 刷新
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()的行为有本质区别。
- 刷新
queueMicrotask回调
const clock = sinon.useFakeTimers({ toFake: ["queueMicrotask"] }); let called = false; queueMicrotask(() => { called = true; }); clock.runMicrotasks(); t.ok(called, "queueMicrotask callback should have run");- 不会触发
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-timers包install()的产物,因此runMicrotasks()的实现来自上游 fake-timers 库;Sinon 的useFakeTimers()只是负责组装配置并把它安装到全局对象上。
异步变体与微任务的交互
tick 系列还提供了异步变体tickAsync()、runAllAsync()、nextAsync()、runToLastAsync()。文档明确指出这些异步版本会中断事件循环,让已调度的 Promise 回调在运行定时器之前执行:
The
tickAsync()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按环境能力收集可替换的全局函数,其中就包括nextTick与queueMicrotask——这正是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.clock与useFakeTimers()返回的 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.
相关推荐
策略综合(Strategy Synthesis):在 machine-learning-for-trading 中用统一流水线横评九大案例研究的信号、组合与成本存活
策略综合(Strategy Synthesis):在 machine learning for trading 中用统一流水线横评九大案例研究的信号、组合与成本
测试开发工具InfoSpider爬虫队列:任务队列的实现与管理策略
InfoSpider爬虫队列:任务队列的实现与管理策略 引言:爬虫任务管理的痛点与解决方案 在数据采集领域,高效的任务调度机制是保障爬虫系统稳定性和性能的核心。
网页爬虫数据分析数据可视化桌面应用brpc ExecutionQueue 深度解析:wait-free 异步串行任务队列的原理与实战
brpc ExecutionQueue 深度解析:wait free 异步串行任务队列的原理与实战 brpc(better RPC)的 bthread 库提供了
后端RPC框架通信网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考