ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在 ts-jest 中正确 Mock ES6 类:default 导出与命名导出的模块模拟实战

在 ts-jest 中正确 Mock ES6 类:default 导出与命名导出的模块模拟实战 测试开发工具【免费下载链接】ts-jestA Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.项目地址https://gitcode.com/gh_mirrors/ts/ts-jest点击查看免费下载导读在 TypeScript Jestts-jest的测试场景中jest.mock()一个默认导出export default的 ES6 类时直接返回一个jest.fn()往往会抛出TypeError: sound_player_1.default is not a constructor。本文以 ts-jest 官方文档《Mock ES6 class》为核心解释该错误产生的转译原理给出 default 导出与命名导出两种场景下的正确 mock 写法并结合仓库源码hoist-jestAST 转换器、端到端测试验证jest.mock的提升机制与模块模拟结构帮助你写出稳定、可维护的单元测试。一、问题背景TypeScript 转译后的模块形态TypeScript 编译器会将.ts文件转译为 JavaScript而 ts-jest 正是通过 TypeScript 编译器 API 完成这一转译的其核心实现在 src/transpilers/typescript/transpile-module.ts。在常见的 CommonJS 目标下export default class SoundPlayer {}会被转译为类似exports.default SoundPlayer的结构。因此当你通过 require 方式拿到模块命名空间对象时const soundPlayer require(./sound-player)实际的类并不在soundPlayer本身而是在它的default属性上创建实例自然要写成new soundPlayer.default()这就是文档中反复强调的“soundPlayer.default才是真正的构造函数”这一背景。模块被转译后import语法本身并不会让测试代码直接获得类你在测试文件里看到的所有关于“模块形状”的认知都必须建立在这个转译产物的基础之上。二、常见错误直接 mock 类导致default is not a constructor很多开发者会按照“直接 mock 一个函数”的直觉来写比如照搬 Jest 文档中针对普通函数/类的常见 mock 写法jest.mock(./sound-player, () { return jest.fn().mockImplementation(() { return { playSoundFile: mockPlaySoundFile } }) })这段代码的问题在于jest.mock的工厂函数整体替换了模块导出。也就是说Jest 会用jest.fn()这个函数去替换整个./sound-player模块而不是替换其中的default导出。转译后的代码在运行时访问soundPlayer.default得到的却是undefined于是立刻抛出TypeError: sound_player_1.default is not a constructor从错误信息sound_player_1.default is not a constructor可以清晰地看到出错点正是访问了模块命名空间对象sound_player_1的default属性soundPlayer.default没有指向一个函数构造函数因此new操作无法执行。三、正确方案mock 必须返回带default属性的对象既然运行时依赖的是soundPlayer.default那么 mock 工厂函数返回的对象就必须包含一个名为default的属性且该属性指向一个函数jest.fn()。修改后如下jest.mock(./sound-player, () { return { default: jest.fn().mockImplementation(() { return { playSoundFile: mockPlaySoundFile, } }), } })这里有两个关键点需要理解外层对象模拟的是“模块导出对象”default这个 key 与转译后的exports.default一一对应保证了new soundPlayer.default()能拿到构造函数jest.fn().mockImplementation()模拟的是“类的实例”mockImplementation返回的对象就是new之后的实例形态其中的playSoundFile等属性/方法需要与被 mock 的类实例保持一致。这样测试代码中通过new SoundPlayer()或new soundPlayer.default()创建出来的对象其playSoundFile方法就会被替换为mockPlaySoundFile从而实现对类实例行为的精确控制而无需真正加载源文件。四、命名导入Named Import的 mock 写法如果你的模块不是默认导出而是命名导出例如import { OAuth2 } from ./oauth那么 mock 时就不能用default这个 key而是要把default替换为导入的模块名也就是OAuth2jest.mock(./oauth, () { return { OAuth2: ... // mock here } })这一规则的本质与 default 导出完全一致Jest 的 mock 工厂函数替换的是整个模块导出对象因此返回对象的属性名必须与转译后模块的导出名exports.OAuth2严格对应。这种“返回对象 命名导出属性”的写法在仓库的端到端测试中可以直接找到实证。在 e2e/typescript-pre-7-compat/tests/hoist.spec.ts 中被测文件通过命名导入使用依赖而 mock 工厂返回的正是与导出名一致的属性import { dependency } from ../src/dependency jest.mock(../src/dependency, () ({ dependency: mocked })) it(hoists Jest mocks before imports, () { expect(dependency).toBe(mocked) })注意这里{ dependency: mocked }的对象结构与第四节介绍的{ OAuth2: ... }完全同构命名导入dependencymock 返回对象里就放一个同名的dependency属性。同理typescript-7-compat目录下也存在结构完全相同的验证用例见 e2e/typescript-7-compat/tests/hoist.spec.ts表明该写法在当前与未来 TypeScript 版本下均被持续回归验证。五、原理纵深jest.mock的提升hoisting机制在编写上述 mock 时你可能会好奇jest.mock出现在import语句之后为什么还能在模块加载时生效这背后是 ts-jest 内置的hoist-jestAST 转换器在起作用。在 src/transformers/hoist-jest.ts 的源码中可以看到转换器维护了一张“需要提升的方法清单”HOIST_METHODS [mock, unmock, enableAutomock, disableAutomock, deepUnmock]它识别jest.mock(...)/jest.unmock(...)等调用表达式并将这些语句排序、提前到所有 import 之前执行无论是否使用全局jest对象还是通过jest/globals导入都会被正确识别和提升该转换器带有独立的version 4版本号用于通知 Jest 在转换器内容变更时不要复用旧缓存源码注释对此有明确说明。整个流程在 website/docs/processing.md 描述的ts-jest处理管线中也有对应环节TypeScript 编译完成后会进入“custom AST transformers”阶段注释明确指出“这里就是进行 jest.mock 提升hoisting以及基于配置的用户自定义转换的地方”。因此你在测试文件里写的jest.mock(./sound-player, () ({ default: jest.fn()... }))会被 ts-jest 自动提升到 import 之前执行mock 工厂函数得以在模块真正被 require 之前完成对导出对象的替换——这正是上面所有 mock 写法能够生效的底层保证。六、常见疑问与补充说明Q1为什么外层是普通对象内层default又必须是jest.fn()外层对象对应模块命名空间exports普通对象即可内层default对应构造函数本身测试中会对其执行new所以必须是一个函数jest.fn()既能被new又能通过mockImplementation定制实例行为。Q2如果类上有多个方法怎么办在mockImplementation返回的实例对象中把需要替换的方法逐一列出即可例如{ playSoundFile: mockPlaySoundFile, playAnother: mockAnother }未列出的方法则不会被 mock。Q3命名导出与 default 导出能否混用可以。若模块同时导出default和其他命名导出mock 工厂返回的对象里同时放default和对应命名 key 即可规则互不冲突一一对应转译后的exports属性。Q4这些写法在 ESM 模式下是否适用需要结合具体运行模式判断。ESM 场景下模块形态、import语义与 CommonJS 不同ts-jest 对 ESM 有独立的配置要求涉及extensionsToTreatAsEsm、useESM等详见 website/docs/guides/esm-support.md且 Jest 运行时对 ES 模块的 mock 支持有其自身限制。本文讨论的“模块导出对象包含default/命名属性”这一核心思想仍然成立但在 ESM 模式下应优先查阅对应模式的官方说明与实际表现。七、小结TypeScript 转译后export default的类位于模块导出对象的default属性上实例化需要new soundPlayer.default()直接返回jest.fn()的 mock 会因soundPlayer.default不是函数而报TypeError: sound_player_1.default is not a constructor正确的 default 导出 mock工厂函数返回{ default: jest.fn().mockImplementation(() ({ ...实例方法 })) }命名导出如OAuth2时把default换成对应的导出名即可{ OAuth2: ... }jest.mock之所以能在 import 之前生效靠的是 ts-jest 内置hoist-jestAST 转换器的语句提升机制src/transformers/hoist-jest.ts仓库端到端测试e2e/typescript-pre-7-compat/tests/hoist.spec.ts为“返回对象需与导出名一致”提供了直接实证。掌握“mock 替换的是整个导出对象”这一原则无论面对 default 导出还是任意数量的命名导出你都能写出与转译产物严格对齐、可长期维护的 Jest mock 代码。赞分享测试开发工具【免费下载链接】ts-jestA Jest transformer with source map support that lets you use Jest to test projects written in TypeScript.项目地址https://gitcode.com/gh_mirrors/ts/ts-jest点击查看免费下载相关推荐OHIF 3.10 uiModalService 迁移指南Modal API 重构与组件化改造实战OHIF 3.10 uiModalService 迁移指南Modal API 重构与组件化改造实战 本指南围绕 OHIF Viewers 3.9 → 3.10测试开发工具同一模块中的默认导出与命名导出ES6 Module 混合导出实战til 知识库同一模块中的默认导出与命名导出ES6 Module 混合导出实战til 知识库 本篇技术指南以 til 仓库中的 javascript/default a文档教程知识库Cal.diy 代码库模块导入导出模式命名导出、生成文件与 Build* 工厂命名实践Cal.diy 代码库模块导入导出模式命名导出、生成文件与 Build 工厂命名实践 导读 本文以 agents/rules/quality imports.后端前端企业应用上一篇终极指南如何用ebook2audiobook将电子书变成专业有声书下一篇ClojureScript与Firebase云函数后端逻辑实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表