Mock modules

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

rs.mock

  • 类型:: <T = unknown>(moduleName: string | Promise<T>, moduleFactory?: (() => Promise<Partial<T>> | 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 方法的第二个参数提供了一个工厂函数,则替换为工厂函数的返回值(工厂函数可以是异步函数,取异步函数 await 后的返回值)作为被 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 的内置模块(如 fspath 等),并且在根目录中有一个 __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 lodashsrc/multiple 模块,他们会被替换为 __mocks__/lodash.jssrc/__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.mockrs.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'), async () => {
      return {
        b: 222,
      };
    });

rs.doMock

  • 类型:: <T = unknown>(moduleName: string | Promise<T>, moduleFactory?: (() => Promise<Partial<T>> | 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', async () => {
  const originalModule = await rs.importActual('./sum');
  return { ...originalModule, sum2: rs.fn() };
});

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.unmockrs.doUnmock