资讯详情

资讯详情

用 Link Seams 在 CommonJS 中 Stub 依赖:Sinon + Proxyquire 实战指南

测试开发工具【免费下载链接】sinonTest spies, stubs and mocks for JavaScript.项目地址https://gitcode.com/gh_mirrors/si/sinon点击查看免费下载Sinon 是 JavaScript 测试中最常用的测试替身test double库但它本身只负责创建和注入 spies、stubs、mocks 与 fakes并不拦截模块加载。本指南讲解如何在 CommonJS 环境下利用link seams链接缝技术通过proxyquire这类工具 hook 进 Node.js 的require机制把被测模块的依赖整体替换成由你完全控制的 Sinon stub从而真正隔离被测系统SUT。读完本文你将掌握目标模块 假依赖的完整测试套路理解其底层原理并清楚它与直接属性替换、ESM 可变命名空间等方案的边界。若想更透彻地理解本文示例与 seams 概念的来龙去脉推荐阅读经典著作Working Effectively with Legacy Code中关于 seams 的章节但这并非阅读本文的必要前提。为什么选择 Link Seams 而非直接替换属性在讲实现之前先厘清一个关键区别Sinon 是 stubbing 库不是模块拦截库。它可以把一个对象上的方法替换为 stub但它无法改变被测模块在加载时通过require拿到的依赖引用。docs/guides/how-to/stub-dependency.md介绍的直接属性替换方案要求被测模块与测试共享同一个依赖对象实例const dependencyModule require(./dependencyModule); sinon.stub(dependencyModule, getSecretNumber).returns(3);这只有在被测模块内部没有对依赖做解构destructuring时才能生效而且依赖必须是可写的普通对象。而 link seams 的思路完全不同它不去碰任何已加载的对象而是在require解析的那一刻拦截它把整个依赖模块换成测试提供的假实现。这相当于在调用点而不是属性上制造替换点适用范围更广也无需修改被测代码。背景为什么 CommonJS 依然值得掌握本指南针对的是由 Node.js 推广开来的 CommonJSCJS模块系统。虽然 ECMAScript 2015 引入了 ES ModulesESM标准但在 2023 年之前 CJS 一直是事实上的主流模块格式而且时至今日许多转译器与打包器仍会把代码输出为 CJS 模块。典型例子就是 TypeScript截至 2023 年它的默认编译输出仍是 CJS。也就是说你写的import foo from ./foo最终可能被转译成const foo require(./foo)。只要目标运行时里实际执行的还是requirelink seams 方案就依然适用。如果你面对的是真正的 ES Modules例如.mjs文件或package.json中type: module且未经过转译请参阅 如何 Stub ES Module imports 一文——ESM 的命名空间对象在规范上是不可变的需要借助esm包的mutableNamespace选项才能让 Sinon 的 stub 生效原理与本指南完全不同。核心思路Hook 进require要让require的底层调用被替换就需要一个能 hook 进模块加载流程的工具。生态中这类工具不少常见的有proxyquire本指南示例选用API 直观传入要替换的模块路径 → 假实现的映射即可rewire偏重于访问模块私有变量也可替换依赖Quibble语法更简洁并且支持作为 ESM loader 使用在 真实世界依赖 Stub 案例 中有配套用法。尽管工具各异底层机制大同小异它们都包装或替换了 Node 的require函数在模块解析阶段按你给出的映射返回假模块。掌握了 proxyquire 的用法切换到其他工具的成本很低。实战示例Stub 掉fs下面是一个完整的、可运行的示例演示如何隔离一个依赖fs的模块。完整的源码与可运行 demo 由 Sinon 团队维护在sinonjs/demo-proxyquire仓库中。示例的项目结构如下. ├── lib │ └── does-file-exist.js └── test └── does-file-exist.test.js被测源码lib/does-file-exist.js这是doesFileExist模块的源码它只有一个依赖fs。var fs require(fs); function doesFileExist(path) { return fs.existsSync(path); } module.exports doesFileExist;注意这里的关键点模块顶层require(fs)拿到的fs对象将在测试中被 proxyquire 整体替换成假对象。这正是 link seam 发挥作用的位置——require语句本身就是那个链接缝。测试文件test/does-file-exist.test.js为了隔离被测模块我们把fsstub 掉提供一个我们完全掌控其行为的fs.existsSync假实现var proxyquire require(proxyquire); var sinon require(sinon); var assert require(referee).assert; var doesFileExist; // the module to test var existsSyncStub; // the fake method on the dependency describe(example, function () { beforeEach(function () { existsSyncStub sinon.stub(); // create a stub for every test // import the module to test, using a fake dependency doesFileExist proxyquire(../lib/does-file-exist, { fs: { existsSync: existsSyncStub, }, }); }); describe(when a path exists, function () { beforeEach(function () { existsSyncStub.returns(true); // set the return value that we want }); it(should return true, function () { var actual doesFileExist(9d7af804-4719-4578-ba1d-5dd8a4dae89f); assert.isTrue(actual); }); }); });拆解测试的关键步骤每次测试前新建 stubexistsSyncStub sinon.stub()生成一个全新的、无预设行为的 stub 函数。把它放在beforeEach中确保不同测试之间互不污染。用 proxyquire 注入假依赖proxyquire(../lib/does-file-exist, { fs: { existsSync: existsSyncStub } })的意思是加载../lib/does-file-exist时但凡它require(fs)就返回{ existsSync: existsSyncStub }这个假对象。于是被测模块里的fs.existsSync(path)实际调用的是我们的 stub。预设行为existsSyncStub.returns(true)让 stub 无论收到什么路径都返回true从而把测试引导到路径存在这一分支。断言assert.isTrue(actual)验证被测函数确实把 stub 的返回值透传了出来。当你想测试路径不存在的分支时只需在另一个describe块里调用existsSyncStub.returns(false)被测模块的代码一行都不用改——这正是隔离被测系统的意义所在。此外stub 继承了 Sinon 完整的 spy API你还可以用existsSyncStub.calledWith(...)、existsSyncStub.callCount等断言验证依赖的调用情况。源码级原理Sinon stub 到底做了什么从实现层面看sinon.stub()的行为可以在仓库源码中得到印证。src/sinon/stub.js 中的stubImpl是 stub 的核心实现它会先检查目标对象是否是 ES Module若是则抛出TypeError: ES Modules cannot be stubbed再通过getPropertyDescriptor读取属性的属性描述符property descriptor校验其可写/可配置性。真正执行替换动作的是 src/sinon/util/core/wrap-method.js 中的wrapMethod——它把对象上的原方法包装成 stub 代理proxy原函数不再被调用。这解释了 link seams 方案与直接属性替换的一个本质差异直接属性替换sinon.stub(fs, existsSync)要求fs上确实存在该属性、且属性描述符允许被修改。如果依赖是转译产物如 SWC 输出的非可配置 getter会直接抛错——docs/guides/how-to/typescript-swc.md中的案例就演示了这一点。proxyquire 方案完全不触碰原fs对象我们提供的假对象本身就是 stub因此不受属性描述符约束适用于任何形式的依赖。此外stub 的returns()行为在 src/sinon/behavior.js 与 src/sinon/default-behaviors.js 中定义调用returns(value)会替换 stub 的默认行为而每次调用都经过createStub中生成的代理函数见 src/sinon/stub.js最终由当前行为behavior决定返回值。与其他依赖替换方案的对比方案适用场景关键限制Link seamsproxyquire / Quibble / rewireCommonJS 模块希望整体替换依赖、不修改被测代码需要额外工具不适用于真正的 ESM直接属性替换sinon.stub(dep, meth)依赖是共享实例、属性可写可配置依赖被解构后失效转译产物可能不可配置显式依赖注入DI任意模块系统被测代码可改需要改造被测代码的签名或入口ESM 可变命名空间esm包真正的 ESM 环境偏离 ESM 规范仅作测试便利其中显式依赖注入与 Quibble 在 link seams 场景的变体在 真实世界依赖 Stub 案例TypeScript SWC 一文中有完整的分步实战演示——那里用quibble(./other, { toBeMocked: mocked })配合sandbox.stub().returns(mocked)本质与本文的 proxyquire 示例同源。局限与注意事项仅适用于 CommonJS 执行路径如果模块最终以真实 ESM 形式运行未转译require拦截不会生效请改用 Stub ES Module imports 中的方案。替换粒度是模块而非属性你给出的假对象需要包含被测模块实际会访问的全部成员否则运行时会得到undefined而不是TypeError错误会更隐蔽。与打包器/转译器输出有关如果 TS/Babel 已把 ESM 编译为 CJS本文方案依然有效但若转译产物把导出变成不可配置的 getter直接属性替换会失败link seams 仍可绕过该问题。在测试中保持 stub 隔离务必在beforeEach中新建 stub并考虑使用sinon.createSandbox()统一管理restore()避免测试间相互泄漏。延伸阅读如何 Stub 模块的某个依赖CommonJS 直接属性替换如何 Stub ES Module importsESM 可变命名空间真实世界依赖 Stub 案例TypeScript SWC QuibbleStubs 概念总览含returns、onCall等行为 APISinon 官方 How-to 索引赞分享测试开发工具【免费下载链接】sinonTest spies, stubs and mocks for JavaScript.项目地址https://gitcode.com/gh_mirrors/si/sinon点击查看免费下载相关推荐SinonJS 实战如何通过链接接缝(Link Seams)模拟 CommonJS 模块SinonJS 实战如何通过链接接缝 Link Seams 模拟 CommonJS 模块 前言 在单元测试中我们经常需要隔离被测系统与其依赖项。本文将深入探测试开发工具Lightly自定义模型开发指南打造专属的自监督学习架构Lightly自定义模型开发指南打造专属的自监督学习架构 Lightly是一个功能强大的Python库专为图像自监督学习设计。本指南将带你了解如何利用Lig深度学习计算机视觉大模型告别JavaScript依赖噩梦RequireJS与CommonJS实战对比告别JavaScript依赖噩梦RequireJS与CommonJS实战对比 你是否还在为网页中杂乱的 script 标签排序抓狂是否经历过因第三方库加载前端上一篇Teku源码编译与调试Java开发者深入理解共识客户端的捷径下一篇终极指南如何用N_m3u8DL-RE高效下载加密流媒体内容创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →