新闻详情

新闻详情

首页 / 资讯中心 / 详情

MikroORM 与 Jest 集成实战:fake timers 与 process.nextTick 冲突的完整解决方案

发布时间:2026/9/26 10:35:07来源:尧图网络
MikroORM 与 Jest 集成实战:fake timers 与 process.nextTick 冲突的完整解决方案
后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载导读在使用 Jest 整理出一套从快速规避到精确控制的完整方案先给出只需一行配置的doNotFake: [nextTick]应急手段再深入讲解各数据库驱动MongoDB、MySQL/MariaDB、PostgreSQL、SQLite/libSQL对process.nextTick()的具体依赖点最后提供可复制的wrappedSpyfakeTimersHooks工具代码让你在需要伪造微任务队列的同时仍能让数据库查询正常执行。读完本文你将能够在 MikroORM Jest 的测试环境中自由控制时钟、验证基于时间的结果缓存过期等逻辑而不再被nextTick卡住。说明本文对应的英文原文档同时存在于仓库的多个版本目录中正文内容基本一致可对照阅读最新版 docs/docs/usage-with-jest.md以及 6.6、7.1、7.2 各版本 docs/versioned_docs/version-6.6/usage-with-jest.md、docs/versioned_docs/version-7.1/usage-with-jest.md、docs/versioned_docs/version-7.2/usage-with-jest.md。为什么 Jest fake timers 会和 MikroORM 打架Jest 的 timer mocks 是测试时间敏感逻辑如限时任务、缓存过期、超时重试的利器。它不仅能伪造setTimeout/setInterval这类定时器让测试代码通过jest.advanceTimersByTime()手动拨动时钟还会伪造 Node.js 的process.nextTick()——而 MikroORM 的多个数据库驱动恰好依赖process.nextTick()完成连接调度、查询结果收尾、错误投递等内部流程。一旦该函数被伪造且回调不被真正执行数据库查询就可能永远等不到结果测试表现为挂起或超时。判断你的代码是否需要真实的process.nextTick()核心依据是你的业务逻辑是对事件循环的 tick 流逝敏感还是只对系统时钟敏感。绝大多数时间敏感测试例如验证结果缓存何时过期只关心时钟走过了多少毫秒并不关心微任务队列的推进节奏——此时采用下面的简单方案即可。快速方案一行配置让 nextTick 保持真实如果你确认自己的代码不依赖事件循环 tick 的推进只依赖系统时钟可以安全地让 Jest 不伪造process.nextTick()jest.useFakeTimers({ doNotFake: [nextTick] });这样你依然可以使用 Jest 其余的全部 timer mock API 来控制系统时钟同时 MikroORM 依赖中的定时器最典型的是结果缓存 result cache的过期计时也能正常工作。仓库中 MikroORM 自身的测试就是这一思路的直接证据在 tests/features/result-cache/result-cache.postgre.test.ts 中测试通过vi.useFakeTimers()开启伪时钟然后以cache: 100毫秒查询实体接着vi.advanceTimersByTime(50)两次确认缓存命中mock.mock.calls仍为 1没有发出新的 SQL最后vi.advanceTimersByTime(1)越过 100ms 边界确认缓存过期后重新发起了查询调用数变为 2tests/features/result-cache/result-cache.mongo.test.ts 对 MongoDB 做了同样的验证。这套测试模式正是只关心时钟、不关心 tick的典型场景与doNotFake: [nextTick]的思路完全吻合。如果你需要对微任务队列进行更精细的控制例如业务代码里用到了process.nextTick且必须伪造它请继续阅读下一节。各数据库驱动对 process.nextTick 的已知依赖原文档逐一梳理了 MikroORM 各支持驱动对process.nextTick()的依赖情况这里整理成速查表方便你判断自己是否处于危险区驱动是否使用连接池process.nextTick 使用点危险程度MongoDB强制使用无法关闭连接池的取连接/还连接调度即使池大小设为 1客户端仍会走连接池高MySQL / MariaDB是连接池、池集群以及查询结果收尾阶段见 mysql2 的lib/commands/query.js中done方法的实现高PostgreSQL是默认连接池非原生客户端处理服务端错误时会在下一个 tick 重新抛出错误包括错误 SQL、读超时等或用户回调抛出的异常中高SQLite否原生 sqlite3 支持缓存数据库实例caching会在下一个 tick 移交缓存的实例但MikroORM 不使用该特性低安全其中几条值得展开说明MySQL/MariaDB 的查询收尾除了池与池集群mysql2 客户端在终结查询结果时也会使用process.nextTick()。这意味着即使你不使用连接池实际上 MikroORM 的 MySQL 驱动默认走池单条查询本身也可能卡在伪造的 nextTick 上。PostgreSQL 的错误投递非原生纯 JS客户端在遇到服务端错误错误的 SQL、读取超时等或用户回调抛出异常时会把这些错误推迟到下一个 tick 重新抛出。因此如果你既不用连接池、也不使用裸 SQL 查询raw SQL理论上可以放心伪造process.nextTick()未捕获异常只会在你手动拨动时间时出现。SQLite 是安全的sqlite3 的缓存实例移交是它唯一使用process.nextTick()的地方而 MikroORM 并未使用这一特性所以用 MikroORM SQLite/libSQL 时永远可以安全地伪造 nextTick。但如果你绕过 MikroORM 直接拿 sqlite3 客户端并使用缓存实例就会撞上 SQLite 这唯一的 nextTick 依赖。进阶方案只在关键时刻放行真实的 nextTick如果你的业务代码确实对微任务队列敏感、必须伪造process.nextTick()而 MikroORM 又在同一进程里使用连接池或 MySQL 驱动那么你就需要仅在关键操作期间使用真实 nextTick操作结束后恢复伪造的精细化 mock 策略。最终效果是你的应用代码与 MikroORM 相关代码如 pre-flush 钩子、自定义类型 JS → DB 的转换逻辑在 nextTick 上排队的回调不会被立即执行它们只会在查询进行期间、查询结果返回之前被执行且可能继续排入新的、可执行也可不执行的 nextTick 回调一旦查询结果返回回调又恢复排队但不执行的状态直到你手动拨动时间。值得注意的是这也涵盖了任何 MikroORM 相关代码在查询期间排队的回调如 post-flush 钩子、自定义类型 DB → JS 的转换逻辑。原文档为此提供了一个经实测可用的工具函数wrappedSpy写作时与当时版本的 Jest 兼容它的作用是为某个对象的方法挂上一个可恢复的一次性 spy在调用原方法前切换为放行 nextTick的时钟配置在原方法同步或 Promise 异步完成后立即切回伪造 nextTick的配置从而把真实 nextTick 的窗口精确限制在数据库操作期间。export function wrappedSpyconst T extends {}, const M extends jest.FunctionPropertyNamesRequiredT( object: T, method: T[M] extends jest.Func ? M : never, hooks: Readonly{ beforeOriginal?: (...args: jest.ArgsTypejest.FunctionPropertiesRequiredT[T[M] extends jest.Func ? M : never]) void, afterOriginal?: (result: ReturnTypeT[M] extends jest.Func ? T[M] : never extends Promiseinfer R ? R : ReturnTypeT[M] extends jest.Func ? T[M] : never) void, errorOriginal?: (error?: unknown) void, } ) { const originalSpy jest.spyOn(object, method); const mockImpl: Parameterstypeof originalSpy.mockImplementationOnce[0] (...args) { hooks.beforeOriginal?.(...args); try { const result (object[method] as Function).apply(originalSpy.mock.contexts.at(-1), args); if (result instanceof Promise) { result.then((v) { hooks.afterOriginal?.(v); return v; }).catch((e) { hooks.errorOriginal?.(e); }).finally(() { originalSpy.mockImplementationOnce(mockImpl!); }); } else { hooks.afterOriginal?.(result); originalSpy.mockImplementationOnce(mockImpl!); } return result; } catch (e) { hooks.errorOriginal?.(e); originalSpy.mockImplementationOnce(mockImpl!); throw e; } }; originalSpy.mockImplementationOnce(mockImpl); return originalSpy; } const finallyHook () { jest.useFakeTimers({ doNotFake: [], now: jest.now() }); }; export const fakeTimersHooks { beforeOriginal: () { jest.useFakeTimers({ doNotFake: [nextTick], now: jest.now() }); }, afterOriginal: finallyHook, errorOriginal: finallyHook, } as const satisfies Parameterstypeof wrappedSpy[2];其工作机制可以拆解为三步jest.spyOn(object, method)创建 spy并用mockImplementationOnce注册一次性实现保证每次调用都先走mockImpl包装层beforeOriginal在进入原方法前调用jest.useFakeTimers({ doNotFake: [nextTick], now: jest.now() })——注意now: jest.now()用于保持当前已拨动的时钟不变只临时放行 nextTickafterOriginal/errorOriginal在方法成功返回含 Promise resolve或抛错后调用finallyHook把时钟配置恢复为doNotFake: []即再次全面伪造包括 nextTick并在finally中重新注册下一次的一次性实现实现每次调用都自动续期。搭配 MySQL / MariaDB 使用如果使用 MySQL 或 MariaDB还需要额外 mock 那些内部使用process.nextTick()的具体方法——包括查询命令的done、连接池的取/还连接、池集群的end等import { resolve, dirname } from node:path; import { fakeTimersHooks, wrappedSpy } from ./nextTickFixer; export function enableFakeTimersWithMikroOrm() { const mysqlDir dirname(require.resolve(mysql2)); return { mocks: [ wrappedSpy(require(resolve(mysqlDir, lib/commands/query.js)).prototype, done, executeHooks), wrappedSpy(require(resolve(mysqlDir, lib/commands/ping.js)).prototype, pingResponse, executeHooks), wrappedSpy(require(resolve(mysqlDir, lib/commands/register_slave.js)).prototype, registerResponse, executeHooks), wrappedSpy(require(resolve(mysqlDir, lib/pool.js)).prototype, getConnection, executeHooks), wrappedSpy(require(resolve(mysqlDir, lib/pool.js)).prototype, releaseConnection, executeHooks), wrappedSpy(require(resolve(mysqlDir, lib/pool_cluster.js)).prototype, end, executeHooks), ], mockRestore: function () { let mock: jest.SpyInstance | undefined; while (mock this.mocks.pop()) { mock.mockRestore(); } } }; }这里通过require.resolve(mysql2)动态定位驱动安装目录再按内部文件路径lib/commands/query.js、lib/pool.js等取到原型对象进行包装避免了硬编码 node_modules 路径带来的脆弱性。注意代码中的executeHooks应替换为从nextTickFixer导入的fakeTimersHooks原文档此处的命名笔误下文 PostgreSQL 片段同理即改为wrappedSpy(..., fakeTimersHooks)。搭配 PostgreSQL 使用PostgreSQL 场景下原文档建议优先考虑引入pg-native依赖以启用原生错误处理从而免去额外的 mock或者检查你的测试实际产生的错误是从哪里抛出的然后针对性 mockpg/client.js中的相应方法。无论是否引入pg-native只要使用了连接池就都需要补上对池的connect方法的包装import Pool from pg-pool; import { fakeTimersHooks, wrappedSpy } from ./nextTickFixer; export function enableFakeTimersWithMikroOrm() { return { mocks: [ wrappedSpy(Pool.prototype, connect, executeHooks), ], mockRestore: function () { let mock: jest.SpyInstance | undefined; while (mock this.mocks.pop()) { mock.mockRestore(); } } }; }同样地executeHooks应替换为fakeTimersHooks。由于pg-pool是 PostgreSQL 驱动使用的连接池实现包装Pool.prototype.connect即可覆盖取连接这一最关键的 nextTick 依赖点。搭配 MongoDB 使用MongoDB 客户端没有不使用连接池的选项因此需要 mock 的面更广——连接池ConnectionPool的构造与全部关键方法以及拓扑管理Topology中的服务器选择与更新处理import { Topology } from mongodb/lib/sdam/topology; import { ConnectionPool } from mongodb/lib/cmap/connection_pool; import { fakeTimersHooks, wrappedSpy } from ./nextTickFixer; function enableFakeTimersWithMikroOrm() { return { mocks: [ wrappedSpy(ConnectionPool, constructor, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, checkIn, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, checkOut, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, clear, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, destroyConnection, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, ensureMinPoolSize, fakeTimersHooks), wrappedSpy(ConnectionPool.prototype, processWaitQueue, fakeTimersHooks), wrappedSpy(Topology.prototype, serverUpdateHandler, fakeTimersHooks), wrappedSpy(Topology.prototype, selectServer, fakeTimersHooks), ], mockRestore: function () { let mock: jest.SpyInstance | undefined; while (mock this.mocks.pop()) { mock.mockRestore(); } } }; }从源码结构看这里覆盖了 MongoDB 连接池生命周期的核心环节constructor池初始化、checkOut/checkIn借出与归还连接、processWaitQueue等待队列调度即下一 tick 分配连接的关键算法、clear/destroyConnection/ensureMinPoolSize清理与最小连接数维护以及Topology层的selectServer服务器选择和serverUpdateHandler服务器状态更新。这些都是process.nextTick()的高频使用点。在测试中使用这些 mock在所有查询之前调用enableFakeTimersWithMikroOrm()即可让数据库操作期间临时放行真实 nextTick测试结束时调用返回对象上的mockRestore()恢复所有 spy从而重新启用完整含 nextTick的伪造时钟——或者反过来确保若之后还有查询被调用测试会冻结而非悄悄继续。完整用法示例如下import { initORM } from ./db; // 参考项目的 ORM 初始化封装见下 import { enableFakeTimersWithMikroOrm } from ./fakeTimersFixer; // 按驱动选择对应版本见上文 test(() { const orm initORM({ // 你的测试配置 }); jest.useFakeTimers(); const ormMock enableFakeTimersWithMikroOrm(); // 正常编写你的测试可以用 jest.advanceTimersByTime() 拨动时钟 // 例如验证 result cache 在过期前后是否会重新发起查询 ormMock.mockRestore(); // 注意原文档示例代码此处写作 restoreMock实际为返回对象上的 mockRestore jest.useRealTimers(); });两点使用提示调用时机enableFakeTimersWithMikroOrm()必须在任何查询发生之前调用因为它的目的是给下一次查询预先装好放行 nextTick 的一次性包装mockRestore()则用于测试收尾时的清理。项目初始化封装示例中的initORM指你项目里封装 MikroORM 初始化MikroORM.init的工具函数可参考仓库文档 docs/docs/guide/03-project-setup.md 中关于项目配置与初始化的章节将其与 Jest 环境如 docs/docs/guide/00-introduction.md 中提到的测试配置组合使用。方案取舍与适用边界小结首选doNotFake: [nextTick]绝大多数只关心时钟、不关心微任务队列的测试都适用一行配置、零侵入MikroORM 的结果缓存等基于定时器的功能参考 docs/docs/caching.md可以照常工作。SQLite/libSQL 无脑安全MikroORM 不使用 sqlite3 的缓存实例特性因此伪造 nextTick 不会影响 SQLite/libSQL 驱动。MongoDB / MySQL / MariaDB / PostgreSQL带连接池需要精细 mock只有当你必须伪造process.nextTick()业务对微任务队列敏感时才需要引入wrappedSpy 各驱动的fakeTimersFixer。PostgreSQL 的额外选项引入pg-native可绕过纯 JS 客户端的 nextTick 错误投递减少 mock 面。需要说明的是仓库当前的主测试框架已迁移到 Vitest如 tests/features/result-cache/result-cache.postgre.test.ts 使用vi.useFakeTimers()/vi.advanceTimersByTime()但本文介绍的 Jest 方案思路完全通用核心矛盾始终是伪造 nextTick 与驱动内部调度之间的冲突解决方案要么绕开doNotFake要么在数据库操作的精确窗口内放行真实 nextTickwrappedSpy。理解这一机理后无论测试框架如何演进你都能快速定位并解决时间敏感测试中查询卡死类问题。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐MikroORM 与 Jest 假定时器Fake Timers的兼容性实战从 nextTick 陷阱到连接池修复方案MikroORM 与 Jest 假定时器Fake Timers的兼容性实战从 nextTick 陷阱到连接池修复方案 在 Node.js 后端项目中用后端PDF补丁丁一站式免费PDF工具箱的终极解决方案PDF补丁丁一站式免费PDF工具箱的终极解决方案 当您面对PDF文档处理的各种难题时是否曾为找不到合适的工具而烦恼PDF补丁丁正是为解决这些痛点而生的开源桌面应用文档Jest 假定时器与 MikroORM 共存实战绕开 process.nextTick() 陷阱的完整指南Jest 假定时器与 MikroORM 共存实战绕开 process.nextTick 陷阱的完整指南 当你使用 Jest 测试基于 MikroORM 的应用后端上一篇VictoriaMetrics 与 AI 工具集成指南MCP 服务器、Agent Skills 与 AI 可观测性实战下一篇Agent Zero 插件实战指南从零创建 unread_dot 本地前端插件并完成评审创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WPF ProgressBar 垂直温度计实现:ControlTemplate 与动画 2026/9/26 13:17:56

WPF ProgressBar 垂直温度计实现:ControlTemplate 与动画

简介:本资源面向WPF开发者与UI控件学习者,聚焦如何借助ProgressBar控件实现垂直温度计效果,解决默认水平进度条难以满足仪表类界面需求的问题。内容围绕Orientation属性设置、ControlTemplate自定义模板、Path与ScaleTransform动态指针、动画…

阅读更多 →
急速搜索劫持:从捆绑安装到彻底清理的完整指南 2026/9/26 13:17:48

急速搜索劫持:从捆绑安装到彻底清理的完整指南

1. 电脑上莫名其妙出现的“急速搜索”到底是什么来头前几天帮一个朋友处理电脑问题,他跟我说:“桌面上突然多了个‘急速搜索’的图标,我根本没装过这东西,删了之后重启又回来了。”我远程连过去一看,任务栏右下角还挂着…

阅读更多 →
老胡的周刊(第195期):TaoToken 统一 Key 接入 Cline 的 settings.json 配置骨架 2026/9/26 13:17:41

老胡的周刊(第195期):TaoToken 统一 Key 接入 Cline 的 settings.json 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Tripo×World Labs黑客松:AI生成3D模型与场景的实战全解析 2026/9/26 13:17:41

Tripo×World Labs黑客松:AI生成3D模型与场景的实战全解析

Tripo 和 World Labs 一起办 3D 黑客松这个消息,我第一反应是:3D 生成赛道终于要动真格的了。过去两年,我们见过了太多“生成一张图”的AI比赛,也见过了不少“生成一个模型”的Demo展示,但让做 3D 模型的人和做 3D 世界…

阅读更多 →
文件式与交互式运行:从五个程序实例看后台密码处理 2026/9/26 13:17:41

文件式与交互式运行:从五个程序实例看后台密码处理

文件式和交互式,这两个词听起来像是教材里才会出现的概念,但我发现很多写了两三年脚本的人,其实也没完全搞明白它们到底意味着什么。最近在群里又看到有人问“shell脚本放在后台执行,还要交互式输入密码怎么处理”,这个…

阅读更多 →
SpringBoot+Vue3前后端分离商城系统实战:从数据库设计到部署上线 2026/9/26 13:17:40

SpringBoot+Vue3前后端分离商城系统实战:从数据库设计到部署上线

去年接了一个服装批发客户的单子,需求很直接:要做一套商城系统,前端能展示商品、加购物车、下单,后台要管商品、订单、库存和会员,还得留出以后接优惠券、拼团这些营销功能的余地。我最终选了SpringBoot Vue3这套组合…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉