Mocking
Mocking 用于在测试中替换依赖、控制返回值,并断言函数或模块的调用行为。Rstest 提供了多组 mock API,分别适用于函数、对象方法、ESM 模块、CommonJS 模块和对象树。
Mock 模块
如果依赖是通过模块系统加载的,可以根据模块类型和 mock 方式选择不同的 API。模块 mock 在 Node 模式与浏览器模式下的行为一致。
Mock ESM 模块
如果依赖是通过 import 加载的,可以使用 rs.mock() 或 rs.doMock()。
使用 rs.mock()
rs.mock() 会被提升到文件顶部,适用于在被测模块执行前就替换依赖的场景。
使用 rs.doMock()
需要注意的是,rs.doMock() 不会被提升,只会在执行之后生效。它适用于前面的 import 保持真实实现,后面的再切换成 mock 的场景。
与提升的 mock factory 共享值
由于 rs.mock() 会被提升,它的 factory 无法读取模块按正常顺序执行时才初始化的变量。当 factory 和测试断言需要共享同一个 mock 函数或值时,可以使用 rs.hoisted()。
Mock CommonJS 模块
如果依赖是通过 require() 加载的,可以使用 rs.mockRequire() 或 rs.doMockRequire()。
这些 API 面向 CommonJS 互操作场景。它们在浏览器测试中同样可用,但编写浏览器测试时优先使用 ESM API(rs.mock / rs.doMock)。
使用 rs.mockRequire()
rs.mockRequire() 会被提升到文件顶部,适用于 CommonJS 模块的文件级 mock 设置。
使用 rs.doMockRequire()
需要注意的是,rs.doMockRequire() 不会被提升,只会影响后续的 require() 调用。
需要注意的是,如果一个包同时提供 ESM 和 CJS 入口,这个区分尤其重要。mock ESM 入口并不会自动影响 CJS 入口,反过来也一样。
自动 mock 模块
如果你希望先把模块中的函数导出替换成 mock 函数,再由测试补充少数导出的行为,可以只传模块路径调用 rs.mock()。Rstest 会先检查 __mocks__ 中是否存在匹配的手写 mock;如果不存在,则回退为自动 mock 该模块。你也可以显式传入 { mock: true },直接请求自动 mock,并跳过手写 mock 查找。
加载 mock 的模块
rs.mock() 用于配置通过 import 加载时获得的模块。如果测试需要直接获得自动 mock 的模块对象,可以使用下面这些 API:
- rs.importMock() 异步加载 ESM 模块并返回 Promise。对于通过
import使用的模块,需要配合await使用。 - rs.requireMock() 同步加载 CommonJS 模块。它适用于通过
require()使用的模块。
这两个 API 都会将导出的函数和嵌套函数替换为 mock。原始值会保留,数组则会变为空数组。在 TypeScript 中,调用 mockReturnValue 等 mock 控制方法之前,需要先将返回的模块传给 rs.mocked(module, true)。选择 API 时应匹配代码实际使用的模块 entry,尤其是一个包分别提供 ESM 和 CommonJS entry 时。
Spy 整个模块
如果你希望保留真实实现,同时提供调用断言能力,可以使用 { spy: true }。
注意,spy 只能追踪通过 export 发起的调用 —— 同一模块内部函数之间的互相调用不会被追踪。
部分 mock 模块
当被 mock 的模块仍需要部分或全部真实导出时,应根据模块的加载方式选择对应的 API:
- 当提升的同步
rs.mock()factory 需要真实导出时,为静态 ESM import 添加with { rstest: 'importActual' }。 - 使用 rs.importActual() 在测试中异步加载原始 ESM 模块。
- 使用 rs.requireActual() 同步加载原始 CommonJS 模块,也可以在同步 mock factory 中使用。
静态 ESM 形式适合只替换一个导出,同时保留其余导出的场景:
需要注意的是,由于 factory 会被提升,与 factory 共享的值必须来自静态 importActual import 或 rs.hoisted()。
复用 __mocks__ 里的手写 mock
如果多个测试会复用同一个 fake 实现,可以把它放进 __mocks__ 目录,并在不传 factory 的情况下直接加载。手写 mock 的优先级高于自动 mock 回退。
重置模块状态
如果你希望后续的 import 或 require() 返回原始模块,可以使用下面这些 API:
- rs.unmock() / rs.doUnmock():停止 mock 通过
import加载的模块。 - rs.unmockRequire() / rs.doUnmockRequire():停止 mock 通过
require()加载的模块。 - rs.resetModules():清空模块缓存,让下一次 import 或 require 重新执行模块。
需要注意的是,rs.resetModules() 不会取消模块 mock。要取消模块 mock,需要根据模块的加载方式选择对应的 unmock API。
关于完整 API 和更多示例,可以参考 Mock modules。
Mock 函数
如果依赖是以回调或注入实现的形式传入的,可以使用 rs.fn() 创建 mock 函数。
你也可以通过 mock 实例方法覆盖行为,例如为某一次调用返回不同的结果:
关于完整 API 和更多示例,可以参考 Mock functions 和 MockInstance。
Spy 现有方法
如果你希望保留真实对象,同时跟踪调用或临时覆盖行为,可以使用 rs.spyOn()。
using 语法
Rstest 支持使用 using 语法在代码块退出时自动恢复 spy:
这类写法常用于 console、Date 这类全局对象,以及测试里已经存在的共享对象。
深度 mock 对象
如果依赖已经存在于内存中,并且你希望把嵌套方法转换成 mock,可以使用 rs.mockObject()。
如果你希望保留嵌套方法的原始实现,同时记录调用,可以传入 { spy: true }。
关于完整 API 和更多示例,可以参考 Mock functions 和 MockInstance。
标注与识别 mock 函数
rs.mocked() 在运行时返回同一个值,只用于告诉 TypeScript 将其视为 mock。自动 mock 的模块或对象仍保留原始静态类型时,可以使用它。
rs.isMockFunction() 会执行运行时检查。当测试逻辑需要判断一个函数当前是否为 mock 时,可以使用它;在 TypeScript 中,它也会将函数类型收窄为 MockInstance。
清理 mock 状态
如果你要处理调用记录残留或 mock 实现残留,可以使用下面这些 API:
- clearMocks:每个测试前清空调用记录。
- resetMocks:清空调用记录并重置 mock 实现。
- restoreMocks:恢复真实对象上被 spy 的描述符。
如果你想手动调用,对应的 API 是 rs.clearAllMocks()、rs.resetAllMocks() 和 rs.restoreAllMocks()。