close
logo
Rstest
指南
配置
API
English
简体中文
指南
配置
API
English
简体中文
logo
Rstest
Overview

Test API

Expect
Test
Describe
Hooks

Rstest Utility

Mock modules
Mock functions
MockInstance
Fake timers
Utilities
📝 在 GitHub 上编辑此页
上一页Rstest Utility
下一页Mock functions

#Mock modules

Rstest 支持对模块进行 mock,这使得你可以在测试中替换模块的实现。Rstest 提供了 rs(别名 rstest)工具函数来进行模块的 mock 。你可以直接使用以下方法来 mock 模块:

#rs.mock

  • 类型:: <T = unknown>(moduleName: string | Promise<T>, moduleFactory?: (() => Partial<T>)) => void

调用 rs.mock 时,Rstest 会对第一个参数对应的模块进行 mock 替换。rs.mock 将根据是否提供了第二个 mock 工厂函数来决定如何处理被 mock 的模块,下面会详细介绍这两种情况。

需要注意的是:rs.mock 会被提升到当前模块的顶部,所以即使在调用 rs.mock('some_module') 前执行了 import fn from 'some_module',some_module 也会在一开始被 mock。

根据提供的第二个参数,rs.mock 有两种行为:

  1. 如果 rs.mock 方法的第二个参数提供了一个工厂函数,则替换为工厂函数的返回值作为被 mock 的模块的实现。
src/sum.test.ts
import { sum } from './sum';

rs.mock('./sum', () => {
  return {
    sum: (a: number, b: number) => a + b + 100,
  };
});

expect(sum(1, 2)).toBe(103); // PASS
src/sum.ts
export const sum = (a: number, b: number) => a + b;
  1. 如果 rs.mock 方法调用时没有提供工厂函数,则会尝试去解析与被 mock 的模块在同级的 __mocks__ 目录下的同名模块,具体的 mock 的解析的规则如下:

    1. 如果被 mock 的模块不是 npm 依赖,并且如果有一个 __mocks__ 文件夹与正在 mock 的文件同级,其中 __mocks__ 文件夹包含一个与被 mock 的模块同名的文件,则 Rstest 将使用该文件作为 mock 的实现。
    2. 如果被 mock 的模块是 npm 依赖,并且在根目录中有一个 __mocks__ 文件夹,其中包含一个与被 mock 的模块同名的文件,则 Rstest 将使用该文件作为 mock 实现。
    3. 如果被 mock 的模块是 Node.js 的内置模块(如 fs、path 等),并且在根目录中有一个 __mocks__ 文件夹,其中包含一个与内置模块同名的文件(如 __mocks__/fs.mjs、__mocks__/path.ts 等),则 Rstest 将使用该文件作为对应的 mock 实现(使用 node: 协议导入内置模块时将忽略 node: 前缀)。

    例如项目中有这样的文件结构:

    ├── __mocks__
    │   └── lodash.js
    ├── src
    │   ├── multiple.ts
    │   └── __mocks__
    │       └── multiple.ts
    └── __test__
        └── multiple.test.ts

    那么在如下的测试文件中尝试 mock lodash 和 src/multiple 模块,他们会被替换为 __mocks__/lodash.js 和 src/__mocks__/multiple.ts 中的实现。

    src/multiple.test.ts
    import { rs } from '@rstest/core';
    
    // lodash is a default export from `__mocks__/lodash.js`
    import lodash from 'lodash';
    
    // multiple is a named export from `src/__mocks__/multiple.ts`
    import { multiple } from '../src/multiple';
    
    rs.mock('lodash');
    rs.mock('../src/multiple');
    
    lodash.random(multiple(1, 2), multiple(3, 4));

    rs.mock 和 rs.doMock 也支持第一个参数传入一个 Promise<T>,并将这个 T 的类型作为第二个工厂函数 await 后的返回值(Promise<T>),这能够让 IDE 获得更好的类型提示,并对工厂函数的返回值做类型校验。传入 Promise<T> 除对类型提示有增强外,对 mock 模块能力没有任何影响。

    // Compared to rs.mock('../src/b', ...) the type is enhanced.
    rs.mock(import('../src/b'), () => {
      return {
        b: 222,
      };
    });

#rs.doMock

  • 类型:: <T = unknown>(moduleName: string | Promise<T>, moduleFactory?: (() => Partial<T>)) => void

与 rs.mock 类似,rs.doMock 也会 mock 模块,但它不会被提升到模块顶部。它会在被执行到时调用,这意味着,如果在调用 rs.doMock 之前已经导入了模块,则该模块不会被 mock,而在调用 rs.doMock 之后导入的模块会被 mock。

src/sum.test.ts
import { rs } from '@rstest/core';
import { sum } from './sum';

it('test', async () => {
    // sum is imported before executing doMock, it's not mocked yet
    expect(sum(1, 2)).toBe(3); // PASS
    rs.doMock('./sum')
    const { sum: mockedSum } = await import('./sum');
    // sum is imported after executing doMock, it's mocked now
    expect(mockedSum(1, 2)).toBe(3); // FAILED
})

#rs.importActual

  • 类型:: <T = Record<string, unknown>>(path: string) => Promise<T>

无视一个模块是否被 mock,导入其原始的模块。如果你想 mock 模块的部分实现,可以使用 rs.importActual 来获取原始模块的实现与 mock 的实现进行合并进行部分 mock。

src/sum.test.ts
rs.mock('./sum');

it('test', async () => {
  const actualModule = await rs.importActual('./sum');
  expect(actualModule.sum(1, 2)).toBe(3);
});

Rstest 也提供了同步的 importActual 能力,允许你在同步的 import 语句中导入未被 mock 的模块实现。使用方法是在 import 语句中添加 with { rstest: 'importActual' } 的 import attributes:

src/api.test.ts
import * as apiActual from './api' with { rstest: 'importActual' };

// Partially mock the './api' module
rs.mock('./api', () => ({
  ...apiActual,
  fetchUser: rs.fn().mockResolvedValue({ id: 'mocked' }),
}));

#rs.importMock

  • 类型:: <T = Record<string, unknown>>(path: string) => Promise<T>

导入一个模块及其所有属性的 mock 实现。

src/sum.test.ts
it('test', async () => {
  const mockedModule = await rs.importMock('./sum');
  expect(mockedModule.sum2(1, 2)).toBe(103);
});

#rs.unmock

  • 类型:: (path: string) => void

取消指定模块的 mock 实现。之后所有对 import 的调用都将返回原始模块,即使它之前已被 mock。与 rs.mock 类似,此调用被提升到文件的顶部,因此它将仅取消在 setupFiles 中执行的模块 mock。

src/sum.test.ts
import { rs } from '@rstest/core';
import { sum } from './src/sum';

rs.unmock('./src/sum');

expect(sum(1, 2)).toBe(3); // PASS
rstest.setup.ts
import { rs } from '@rstest/core'
;
rs.mock('./src/sum', () => {
  return {
    sum: (a: number, b: number) => a + b + 100,
  };
});

#rs.doUnmock

  • 类型:: (path: string) => void

与 rs.unmock 相同,但不会被提升到文件顶部。模块的下一次导入将导入原始模块而不是 mock。这不会取消 mock 之前导入的模块。

#rs.resetModules

  • 类型:: resetModules: () => RstestUtilities

清除所有模块的缓存。这允许在重新导入时重新执行模块。这在隔离不同测试中共享的模块的状态时非常有用。

WARNING

不会重置被 mock 的 modules。要清除 mock 的模块,请使用 rs.unmock 或 rs.doUnmock 。