Mock modules

Rstest supports mocking modules, which allows you to replace the implementation of modules in tests. Rstest provides utility functions in rs (rstest) for mocking modules. You can directly use the following methods to mock modules:

rs.mock

  • Type: <T = unknown>(moduleName: string, moduleFactory?: () => T) => void

When calling rs.mock, Rstest will mock and replace the module specified in the first parameter. rs.mock will determine how to handle the mocked module based on whether a second mock factory function is provided, as explained in detail below.

Note that: rs.mock is hoisted to the top of the current module, so even if you execute import fn from 'some_module' before calling rs.mock('some_module'), some_module will be mocked from the beginning.

Based on the second parameter provided, rs.mock has two behaviors:

  1. If a factory function is provided as the second parameter to rs.mock, the module will be replaced with the return value of the factory function (the factory function can be async, use the awaited return value) as the implementation of the mocked module.
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. If rs.mock is called without providing a factory function, it will attempt to resolve a module with the same name in the __mocks__ directory at the same level as the mocked module. The specific mock resolution rules are as follows:

    1. If the mocked module is not an npm dependency, and there is a __mocks__ folder at the same level as the file being mocked, where the __mocks__ folder contains a file with the same name as the mocked module, Rstest will use that file as the mock implementation.
    2. If the mocked module is an npm dependency, and there is a __mocks__ folder in the root directory that contains a file with the same name as the mocked module, Rstest will use that file as the mock implementation.
    3. If the mocked module is a Node.js built-in module (such as fs, path, etc.), and there is a __mocks__ folder in the root directory that contains a file with the same name as the built-in module (e.g., __mocks__/fs.mjs, __mocks__/path.ts, etc.), Rstest will use that file as the corresponding mock implementation (when using the node: protocol to import built-in modules, the node: prefix will be ignored).

    For example, if the project has the following file structure:

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

    Then in the following test file, when trying to mock the lodash and src/multiple modules, they will be replaced with implementations from __mocks__/lodash.js and 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.doMock

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

Similar to rs.mock, rs.doMock also mocks modules, but it is not hoisted to the top of the module. It is called when it's executed, which means that if a module has already been imported before calling rs.doMock, that module will not be mocked, while modules imported after calling rs.doMock will be mocked.

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

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

Imports the original module regardless of whether it has been mocked. If you want to partially mock a module, you can use rs.importActual to get the original module's implementation and merge it with the mock implementation for partial mocking.

src/sum.test.ts
rs.mock('./sum', async () => {
  const originalModule = await rs.importActual('./sum');
  return { ...originalModule, sum2: rs.fn() };
});

rs.importMock

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

Imports a module and all its properties as mock implementations.

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

rs.unmock

  • Type: (path: string) => void

Cancels the mock implementation of the specified module. After this, all calls to import will return the original module, even if it was previously mocked. Like rs.mock, this call is hoisted to the top of the file, so it will only cancel module mocks executed in setupFiles.

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

  • Type: (path: string) => void

Same as rs.unmock, but it is not hoisted to the top of the file. The next import of the module will import the original module instead of the mock. This will not cancel modules that were imported before the mock.

rs.resetModules

  • Type: resetModules: () => RstestUtilities

Clears the cache of all modules. This allows modules to be re-executed when re-imported. This is useful for isolating the state of modules shared between different tests.

WARNING

Does not reset mocked modules. To clear mocked modules, use rs.unmock or rs.doUnmock.