使用 Sinon 对 ES Module 导入进行 Stub:esm 包与 mutableNamespace 完整实战指南
测试开发工具【免费下载链接】sinonTest spies, stubs and mocks for JavaScript.项目地址https://gitcode.com/gh_mirrors/si/sinon点击查看免费下载ES ModulesESM的绑定是**静态解析、实时live且不可变immutable**的因此直接对 ES 模块的命名空间对象执行sinon.stub()会抛出TypeError: ES Modules cannot be stubbed。本文以 Sinon 开源仓库的官方指南 docs/guides/how-to/stub-esm.md 为主体结合 stub.js 与 is-es-module.js 等源码实现讲解如何借助 Node.js 生态中的esm包及其mutableNamespace选项让模块命名空间变为可写从而在 ESM 语境下正常使用 Sinon stub。读完本文你将掌握一套可直接落地的 ESM 单测替身test double方案并理解其底层原理与边界限制。问题本质ESM 命名空间为什么无法被 StubECMAScript 规范规定模块命名空间对象Module Namespace Object的属性是non-writable不可写、non-configurable不可配置、non-deletable不可删除的。也就是说一旦模块加载完成其导出绑定就是只读的任何试图在运行时改写导出的行为都会被 JavaScript 引擎拒绝。Sinon 的stub()在实现上会主动检测这种场景。查看核心实现 stub.jsfunction stubImpl(object, property, context) { if (isEsModule(object)) { throw new TypeError(ES Modules cannot be stubbed); } // ... }而判定是否 ES 模块的逻辑在 is-es-module.js 中export default function isEsModule(object) { return ( object typeof Symbol ! undefined object[Symbol.toStringTag] Module Object.isSealed(object) ); }即如果一个对象的Symbol.toStringTag为Module且对象处于密封sealed状态Sinon 就认定其为 ES 模块命名空间并拒绝创建 stub。这一行为也被仓库的单元测试明确锁定见 stub-test.jsit(throws when trying to stub an ES module namespace object, function () { const object {}; Object.defineProperty(object, Symbol.toStringTag, { value: Module, }); Object.seal(object); assert.exception( function () { createStub(object); }, { name: TypeError, message: ES Modules cannot be stubbed, }, ); });换句话说这是 Sinon 主动抛出的、可读性良好的错误提示目的是避免用户在不可变命名空间上做无效操作后产生困惑。一个典型的失败示例假设有如下源码文件与测试源文件src/math.mjsexport function add(a, b) { return a b; }被测模块src/calculator.mjsimport { add } from ./math.mjs; export function calculate(a, b) { return add(a, b); }测试文件test/calculator.test.mjsimport sinon from sinon; import * as mathModule from ../src/math.mjs; import { calculate } from ../src/calculator.mjs; describe(calculator, () { it(should use the add function, () { // This will throw: TypeError: ES Modules cannot be stubbed sinon.stub(mathModule, add).returns(99); }); });运行测试时会得到TypeError: ES Modules cannot be stubbed。原因正如上文所述mathModule是原生 ESM 命名空间对象其add属性是只读的sinon.stub()无法完成属性替换。解决方案esm包 mutableNamespaceesm包是一个为 Node.js 提供的高性能、生产可用的 ES 模块加载器。它提供了mutableNamespace选项能够将模块命名空间对象包装为可写这正是 Sinon 安装 stub 所需的先决条件。Step 1安装esm包npm install --save-dev esmStep 2创建加载器 / 启动文件在项目根目录创建esm-loader.cjs开启mutableNamespace选项// esm-loader.cjs require require(esm)(module, { cjs: true, mutableNamespace: true, });注意文件必须使用.cjs扩展名或确保package.json中没有type: module从而保证该文件被当作 CommonJS 处理否则无法调用require(esm)。Step 3在运行测试时注册加载器在package.json的test脚本中使用--require参数在测试运行器启动前加载上述启动文件{ scripts: { test: mocha --require ./esm-loader.cjs test/**/*.test.mjs } }Step 4编写测试现在可以像操作普通对象一样对 ES 模块的导出进行sinon.stub()// test/calculator.test.mjs import sinon from sinon; import * as mathModule from ../src/math.mjs; import { calculate } from ../src/calculator.mjs; import assert from assert; describe(calculator, () { afterEach(() { sinon.restore(); }); it(should delegate to the add function, () { sinon.stub(mathModule, add).returns(99); const result calculate(1, 2); assert.equal(result, 99); assert.ok(mathModule.add.calledOnce); }); });注意两点关键约定测试中必须使用import * as mathModule这种命名空间导入方式而不是import { add }解构导入原因见下文局限与注意事项使用afterEach(() sinon.restore())保证每个用例结束后恢复所有被替换的属性避免测试间相互污染这也是 error-handling.md 中反复强调的最佳实践。完整示例项目布局与全部代码. ├── src │ ├── math.mjs │ └── calculator.mjs ├── test │ └── calculator.test.mjs ├── esm-loader.cjs └── package.jsonpackage.json{ name: esm-sinon-example, version: 1.0.0, scripts: { test: mocha --require ./esm-loader.cjs test/**/*.test.mjs }, devDependencies: { esm: ^3.2.25, mocha: ^10.0.0, sinon: * } }esm-loader.cjsrequire require(esm)(module, { cjs: true, mutableNamespace: true, });src/math.mjsexport function add(a, b) { return a b; }src/calculator.mjsimport { add } from ./math.mjs; export function calculate(a, b) { return add(a, b); }test/calculator.test.mjsimport sinon from sinon; import * as mathModule from ../src/math.mjs; import { calculate } from ../src/calculator.mjs; import assert from assert; describe(calculator, () { afterEach(() { sinon.restore(); }); it(should use stubbed add function, () { sinon.stub(mathModule, add).returns(42); const result calculate(10, 20); assert.equal(result, 42); assert.ok(mathModule.add.calledOnceWith(10, 20)); }); it(should call the real add function when not stubbed, () { const result calculate(3, 4); assert.equal(result, 7); }); });第二个用例印证了 stubbing 的可恢复性在afterEach中调用sinon.restore()之后calculate会重新调用真实的add返回真实结果7。为什么这套方案能生效esm包会挂钩 Node.js 的模块加载系统。当设置了mutableNamespace: true时它用Proxy包装 ES 模块命名空间对象使属性赋值得以通过。于是sinon.stub()在命名空间对象上替换属性的操作不再是向不可变对象写入而是向代理对象写入因此不再抛出异常。从 Sinon 源码角度看stubImpl在创建 stub 前会调用 get-property-descriptor.js 获取属性的描述符并对属性描述符做合法性校验见 stub.js 与 is-property-configurable.js。当命名空间经 Proxy 变得可写可配置后属性描述符校验自然通过stub 安装成功。整个链路可以概括为esm加载器 →mutableNamespace启用 Proxy 包装 →sinon.stub(namespace, prop)的属性替换合法化 → stub 生效、调用记录被 spy 收集。局限与注意事项只对esm包有效。原生的--experimental-vm-modules或其他 loader 默认不支持mutableNamespace语义此方案无法迁移到这些环境。转译场景无需此方案。如果使用 TypeScript 或 Babel 且已经将 ESM 编译为 CommonJS那么模块导出变成可变的普通对象直接按 CommonJS 依赖替换的方式处理即可无需esm包。此时请参考 如何 Stub CommonJS 依赖。解构导入无法被 Stub。如果被测模块内部使用import { add } from ./math.mjs并把add作为局部绑定直接调用那么对命名空间对象的 stub不会影响这个早已捕获的局部绑定。要让 stub 生效被测代码必须通过命名空间对象访问导出例如import * as math from ./math.mjs后调用math.add(...)。同理测试文件中也应使用import * as mathModule而非解构导入。mutableNamespace是非标准的。它偏离了 ESM 规范本质上是为测试便利而打破只读约束的手段不应被当作生产环境的常规技巧。生产代码请严格遵守 ESM 只读语义。stub 是库级行为而非模块拦截。Sinon 是一个 stubbing 库而不是模块拦截库依赖替换高度依赖运行环境与实现方式。Node 环境下更通用的推荐做法是link seams或显式依赖注入详见 link-seams-commonjs.md 与 stub-dependency.md。补充替代思路当你不便引入esm包时error-handling.md 还提供了两种轻量替代方案包装对象法把单个导入的函数包进一个普通对象再 stubimport { someMethod } from ./my-module.js; const wrapper { someMethod }; sinon.stub(wrapper, someMethod);sinon.replace()法使用sinon.replace()替换命名空间上的方法import * as myModule from ./my-module.js; import * as sinon from sinon; const fake sinon.fake.returns(mocked value); sinon.replace(myModule, someMethod, fake);这两种方式都可以避免对不可变命名空间直接写入适合不想引入额外加载器的简单场景。相关文章如何 Stub 模块的依赖CommonJS使用 link seams 替换 CommonJS 模块真实世界依赖替换TypeScript SWCStub 错误处理与最佳实践小结ESM 的只读绑定决定了sinon.stub()无法直接作用于原生模块命名空间但通过esm包的mutableNamespace选项配合--require启动加载器可以在一套完整的 Node.js 测试链路中恢复可写命名空间让 Sinon 的 spy/stub/restore 机制在 ESM 项目里正常工作。使用时务必牢记三条红线只对esm包有效、转译场景不需要、解构导入无法生效并结合sinon.restore()保持测试隔离。对于更复杂的依赖替换诉求优先考虑 link seams 或依赖注入等环境无关的方案。赞分享测试开发工具【免费下载链接】sinonTest spies, stubs and mocks for JavaScript.项目地址https://gitcode.com/gh_mirrors/si/sinon点击查看免费下载相关推荐Flow 严格 ES Module 导入/导出 Lint 规则完整指南experimental.strict_es6_import_export 详解Flow 严格 ES Module 导入/导出 Lint 规则完整指南 experimental.strict_es6_import_export 详解 本文开发工具静态分析代码质量Sinon fake timers 进阶clock.runToLast() / runToLastAsync() 完整实战指南Sinon fake timers 进阶 clock.runToLast / runToLastAsync 完整实战指南 本篇技术指南聚焦 Sinon.JS测试开发工具使用PuLP进行模型导入导出的完整指南使用PuLP进行模型导入导出的完整指南 前言 PuLP作为Python中流行的线性规划建模工具提供了强大的模型构建和求解能力。在实际应用中我们经常需要将构建科学计算上一篇如何用Open3D实现点云降维PCA与t-SNE可视化的完整指南下一篇HoRNDIS终极指南5分钟实现Mac与Android的USB网络共享创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

RT-Thread 在 QEMU VExpress-A9 上的运行指南:BSP 编译、启动脚本、SD 卡文件系统与调试全解析

RT-Thread 在 QEMU VExpress-A9 上的运行指南:BSP 编译、启动脚本、SD 卡文件系统与调试全解析

操作系统嵌入式物联网嵌入式OSRTOS 【免费下载链接】rt-thread RT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/ 项目地址: https://gitcode.com/gh_mirrors/rt/rt-thread 点击查看 免费下载 本文围绕…

2026/9/25 5:41:31 阅读更多 →
GoogleTest Matchers 完全参考:用 gperftools 项目中的 gmocking 匹配器写出可读、可复用的断言

GoogleTest Matchers 完全参考:用 gperftools 项目中的 gmocking 匹配器写出可读、可复用的断言

性能剖析内存管理开发工具 【免费下载链接】gperftools Main gperftools repository 项目地址: https://gitcode.com/gh_mirrors/gp/gperftools 点击查看 免费下载 Matchers(匹配器)是 GoogleTest / GoogleMock 中用于校验单个参数的利器&am…

2026/9/25 5:41:31 阅读更多 →
jc --needrestart:把 needrestart -b 的开机重启建议输出转换为 JSON/YAML 的解析器

jc --needrestart:把 needrestart -b 的开机重启建议输出转换为 JSON/YAML 的解析器

开发工具 【免费下载链接】jc CLI tool and python library that converts the output of popular command-line tools, file-types, and common strings to JSON, YAML, or Dictionaries. This allows piping of output to tools like jq and simplifying automation scripts.…

2026/9/25 5:41:31 阅读更多 →

最新新闻

VisiData 列系统深度指南:Column 计算引擎、类型系统与聚合器实战

VisiData 列系统深度指南:Column 计算引擎、类型系统与聚合器实战

数据分析CLI数据可视化 【免费下载链接】visidata A terminal spreadsheet multitool for discovering and arranging data 项目地址: https://gitcode.com/gh_mirrors/vi/visidata 点击查看 免费下载 导读:本文围绕 VisiData 的 Column 体系展开&#…

2026/9/25 6:04:47 阅读更多 →
Plannotator External Annotations API:把外部工具的标注实时推送到活动评审会话

Plannotator External Annotations API:把外部工具的标注实时推送到活动评审会话

【免费下载链接】plannotator Annotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click. 项目地址: https://gitcode.com/gh_mirrors/pl/plannotator 点击查看 免费下载 Plannotator 的 E…

2026/9/25 6:04:47 阅读更多 →
pylibcudf 字符串 API 实战指南:capitalize / title / is_title 的用法与底层原理

pylibcudf 字符串 API 实战指南:capitalize / title / is_title 的用法与底层原理

数据分析数据工程机器学习 【免费下载链接】cudf cuDF - GPU DataFrame Library 项目地址: https://gitcode.com/gh_mirrors/cu/cudf 点击查看 免费下载 cuDF 的 pylibcudf 是 libcudf 的 Cython 绑定层,为 GPU 上的字符串处理提供直接且低开销的 Pyth…

2026/9/25 6:04:47 阅读更多 →
Kubebuilder 项目路线图全景解读:2024–2026 战略规划与源码落地

Kubebuilder 项目路线图全景解读:2024–2026 战略规划与源码落地

开发者工具代码生成CLI云原生后端 【免费下载链接】kubebuilder Kubebuilder - SDK for building Kubernetes APIs using CRDs 项目地址: https://gitcode.com/gh_mirrors/ku/kubebuilder 点击查看 免费下载 本指南以仓库 roadmap/ 目录中的官方路线图文档为核心&a…

2026/9/25 6:04:47 阅读更多 →
Xred木马深度剖析:传播链路、窃密行为与终端应急响应实战

Xred木马深度剖析:传播链路、窃密行为与终端应急响应实战

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

2026/9/25 6:04:47 阅读更多 →
Atlas 300V Pro部署YOLO实战:昇腾推理卡模型转换与调优指南

Atlas 300V Pro部署YOLO实战:昇腾推理卡模型转换与调优指南

一块Atlas加速卡,到底算不算“运算加速卡”?这个问题我在不少群里见人问过,尤其是当你说到“atlas 300V 24G”这个型号的时候,很多人第一反应是:24G显存,那是不是类似游戏显卡那样做渲染加速的?…

2026/9/25 6:03:47 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →