news 2026/10/7 2:03:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 ts-jest 中正确 Mock ES6 类:default 导出与命名导出的模块模拟实战
  • 测试
  • 开发工具

【免费下载链接】ts-jest

A 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 + Jest(ts-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, } }), } })

这里有两个关键点需要理解:

  1. 外层对象模拟的是“模块导出对象”:default这个 key 与转译后的exports.default一一对应,保证了new soundPlayer.default()能拿到构造函数;
  2. 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替换为导入的模块名,也就是OAuth2:

jest.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: ... }完全同构:命名导入dependency,mock 返回对象里就放一个同名的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-jest

A 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
点击查看免费下载
上一篇:终极指南:如何用ebook2audiobook将电子书变成专业有声书
下一篇:ClojureScript与Firebase云函数:后端逻辑实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:02:53

agent-skills 实战:用 skills CLI 为 Claude Code 打造标准化技能包

1. 从"agent-skills"这个标题能读出什么第一次看到agent-skills这个仓库名,我的直觉是:这不是又一个"提示词大全",而是一套把 AI coding agent 的能力拆成可复用模块的工程化尝试。关键词里同时出现了skills CLI、Claude…

作者头像 李华
网站建设 2026/10/7 2:02:19

rkisp驱动代码解析:从V4L2框架到视频调试实战指南

简介:RK ISP 驱动代码包,面向嵌入式Linux下Rockchip图像信号处理器(ISP)的驱动开发与移植场景,适合内核驱动工程师和学习V4L2框架的开发者。资源以rk-isp11为例,重点展示设备树匹配使用的of_device_id&…

作者头像 李华
网站建设 2026/10/7 2:01:11

Agent-Reach 实战:让 AI Agent 真正触达 CLI、文件与远程服务

Agent-Reach 这个名字第一次看到的时候,我下意识以为又是一个套壳的聊天机器人项目。直到把它拉下来跑通第一个任务,才发现它解决的是一个非常具体、也非常痛的问题:让 AI Agent 真正能"够得着"外部世界。这里的 Reach,…

作者头像 李华