close
  • English
  • Mocking

    Mocking lets you replace dependencies in tests, control return values, and assert how functions or modules are called. Rstest provides different mocking APIs for functions, object methods, ESM modules, CommonJS modules, and object trees.

    Mock modules

    If a dependency is loaded through the module system, you can choose different APIs based on the module type and mocking behavior.

    Mock ESM modules

    If a dependency is loaded through import, you can use rs.mock() or rs.doMock().

    Use rs.mock()

    rs.mock() is hoisted to the top of the file. It is useful when the dependency should be replaced before the module under test runs.

    user-service.test.ts
    import { expect, rs, test } from '@rstest/core';
    import { loadUserName } from './user-service';
    import { fetchUser } from './api';
    
    rs.mock('./api', () => ({
      fetchUser: rs.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
    }));
    
    test('returns the fetched user name', async () => {
      await expect(loadUserName('1')).resolves.toBe('Alice');
      expect(fetchUser).toHaveBeenCalledWith('1');
    });

    Use rs.doMock()

    Note that rs.doMock() is not hoisted and only takes effect after it runs. It is useful when earlier import statements should keep the real implementation and later ones should use the mock.

    feature.test.ts
    import { expect, rs, test } from '@rstest/core';
    import { readFeatureFlag } from './feature';
    
    test('only mocks later imports', async () => {
      expect(readFeatureFlag()).toBe('real');
    
      rs.doMock('./feature', () => ({
        readFeatureFlag: () => 'mocked',
      }));
    
      const { readFeatureFlag: mockedReadFeatureFlag } = await import('./feature');
      expect(mockedReadFeatureFlag()).toBe('mocked');
    });

    Mock CommonJS modules

    If a dependency is loaded through require(), you can use rs.mockRequire() or rs.doMockRequire().

    Use rs.mockRequire()

    rs.mockRequire() is hoisted to the top of the file. It is useful for file-level mocking of CommonJS modules.

    math.test.cjs
    const { expect, rs, test } = require('@rstest/core');
    const { sum } = require('./math.cjs');
    
    rs.mockRequire('./math.cjs', () => ({
      sum: (a, b) => a + b + 100,
    }));
    
    test('mocks a CommonJS module loaded with require', () => {
      expect(sum(1, 2)).toBe(103);
    });

    Use rs.doMockRequire()

    Note that rs.doMockRequire() is not hoisted and only affects later require() calls.

    Note that this distinction matters when a package exposes both ESM and CommonJS entries. Mocking the ESM entry does not automatically affect the CommonJS entry, and vice versa.

    Auto-mock modules

    If you want to replace the module's function exports with mock functions first and then configure only selected exports in the test, call rs.mock() with only the module path. Rstest first checks for a matching manual mock in __mocks__; when none exists, it falls back to auto-mocking the module. You can also pass { mock: true } explicitly to skip the manual mock lookup and request auto-mocking directly.

    math.test.ts
    import { expect, rs, test } from '@rstest/core';
    import { add } from './math';
    
    rs.mock('./math');
    
    test('overrides one export', () => {
      rs.mocked(add).mockReturnValue(100);
      expect(add(1, 2)).toBe(100);
    });

    Spy on a whole module

    If you want to keep the real implementation and still assert calls, you can use { spy: true }.

    calculator.test.ts
    import { expect, rs, test } from '@rstest/core';
    import { calculate } from './calculator';
    
    rs.mock('./calculator', { spy: true });
    
    test('keeps the real implementation while tracking calls', () => {
      expect(calculate(1, 2)).toBe(3);
      expect(calculate).toHaveBeenCalledWith(1, 2);
    });

    Note that the spy only tracks calls made through an export — calls between functions inside the same module are not tracked.

    Partially mock modules

    If you want one export to keep the real implementation and another export to be replaced, you can use importActual.

    date-utils.test.ts
    import { expect, rs, test } from '@rstest/core';
    import * as actualDateUtils from './date-utils' with { rstest: 'importActual' };
    import { formatDate, parseDate } from './date-utils';
    
    rs.mock('./date-utils', () => ({
      ...actualDateUtils,
      formatDate: rs.fn().mockReturnValue('2026-03-19'),
    }));
    
    test('keeps parseDate real', () => {
      expect(formatDate(new Date())).toBe('2026-03-19');
      expect(parseDate('2026-03-19')).toBeInstanceOf(Date);
    });

    Note that factory functions are hoisted, so they should not read values that are initialized later in the same file.

    Reuse manual mocks from __mocks__

    If multiple tests reuse the same fake implementation, you can place it in __mocks__ and load it without passing a factory. Manual mocks take precedence over the auto-mock fallback.

    src/
      api.ts
      __mocks__/
        api.ts
    tests/
      user-service.test.ts
    user-service.test.ts
    import { rs } from '@rstest/core';
    
    rs.mock('../src/api');

    Reset module state

    If you want later import or require() calls to return the original module again, you can use these APIs:

    Note that rs.resetModules() does not cancel module mocking. To cancel module mocking, use the matching unmock API for the way the module is loaded.

    For the full API and more examples, see Mock modules.

    Mock functions

    If a dependency is passed in as a callback or injected implementation, you can use rs.fn() to create a mock function.

    user.test.ts
    import { expect, rs, test } from '@rstest/core';
    
    test('passes the selected id to the callback', () => {
      const onSelect = rs.fn();
    
      onSelect('user-1');
    
      expect(onSelect).toHaveBeenCalledTimes(1);
      expect(onSelect).toHaveBeenCalledWith('user-1');
    });

    You can also override behavior through mock instance methods, for example by returning a different value for one call:

    const fetchUser = rs.fn(async (id: string) => ({ id, role: 'guest' }));
    
    fetchUser.mockResolvedValueOnce({ id: '1', role: 'admin' });

    For the full API and more examples, see Mock functions and MockInstance.

    Spy on existing methods

    If you want to keep the real object and still track calls or temporarily override behavior, you can use rs.spyOn().

    logger.test.ts
    import { expect, rs, test } from '@rstest/core';
    
    test('logs a warning when validation fails', () => {
      const warn = rs.spyOn(console, 'warn').mockImplementation(() => undefined);
    
      console.warn('invalid payload');
    
      expect(warn).toHaveBeenCalledWith('invalid payload');
      warn.mockRestore();
    });

    using syntax

    Rstest supports the using syntax to restore the spy automatically when the block exits:

    logger.test.ts
    import { expect, rs, test } from '@rstest/core';
    
    test('logs a warning when validation fails', () => {
      {
        using warn = rs.spyOn(console, 'warn').mockImplementation(() => undefined);
    
        console.warn('invalid payload');
    
        expect(warn).toHaveBeenCalledWith('invalid payload');
      }
    
      // console.warn is restored here
    });

    This pattern is commonly used with globals such as console and Date, as well as shared objects that already exist in the test.

    Deep-mock objects

    If a dependency already exists in memory and you want to convert nested methods into mocks, you can use rs.mockObject().

    service.test.ts
    import { expect, rs, test } from '@rstest/core';
    
    test('mocks nested methods', async () => {
      const service = rs.mockObject({
        user: {
          fetch: async (id: string) => ({ id, name: 'real' }),
        },
        version: 'v1',
      });
    
      service.user.fetch.mockResolvedValue({ id: '1', name: 'mocked' });
    
      expect(service.version).toBe('v1');
      await expect(service.user.fetch('1')).resolves.toEqual({
        id: '1',
        name: 'mocked',
      });
    });

    If you want to keep the original nested implementations while still recording calls, you can pass { spy: true }.

    For the full API and more examples, see Mock functions and MockInstance.

    Clear mock state

    If you need to clear call history or reset mock implementations, you can use these APIs:

    • clearMocks: clear call history before each test.
    • resetMocks: clear call history and reset mock implementations.
    • restoreMocks: restore spied descriptors on real objects.

    For manual cleanup, the corresponding APIs are rs.clearAllMocks(), rs.resetAllMocks(), and rs.restoreAllMocks().

    Further reading