Модули mocks и их очистка

Тестирование компонентов в React почти всегда упирается в работу с зависимостями: API-клиентами, утилитами, хранилищами состояния, сторонними библиотеками. Управление такими зависимостями осуществляется через модули mocks, а их корректная очистка — критически важная часть надёжной тестовой среды.

React Testing Library отвечает за рендеринг и взаимодействие с компонентами, но не занимается подменой зависимостей. Эту задачу берёт на себя тестовый раннер, чаще всего Jest. Именно модульные mocks Jest используются для изоляции компонентов.

jest.mock('../api/userService', () => ({
  getUser: jest.fn(),
}));

Такой mock полностью подменяет импортируемый модуль во всех тестах файла. Компонент при этом ничего не знает о подмене — он работает с тем же интерфейсом.

Ключевой момент: jest.mock применяется на уровне модуля, а не внутри теста. Подмена происходит до выполнения импортов.

Автоматическое поднятие (hoisting) mocks

Вызовы jest.mock поднимаются вверх файла и выполняются до всех import. Это означает:

  • нельзя использовать переменные, объявленные ниже
  • нельзя динамически вычислять mock без фабрики
jest.mock('../config', () => ({
  API_URL: 'http://test.local',
}));

Если требуется доступ к данным во время выполнения, используется фабрика:

jest.mock('../utils/logger', () => {
  return {
    log: jest.fn(),
  };
});

Фабрика выполняется один раз на файл, а не на каждый тест.

Частичное мокирование модуля

Иногда требуется сохранить реальное поведение части модуля.

jest.mock('../math', () => {
  const actual = jest.requireActual('../math');
  return {
    ...actual,
    sum: jest.fn(),
  };
});

Такой подход позволяет тестировать интеграцию, не разрушая контракт модуля целиком.

jest.spyOn и его особенности

jest.spyOn используется для подмены конкретного метода объекта или модуля без полной замены.

import * as api from '../api/userService';

jest.spyOn(api, 'getUser').mockResolvedValue({ id: 1 });

Преимущества:

  • сохраняется реальная структура модуля
  • легко восстановить оригинальную реализацию

Ограничение: метод должен существовать и быть доступен для записи.

Очистка состояния mocks

Каждый mock хранит состояние: количество вызовов, аргументы, возвращаемые значения. Без очистки это состояние протекает между тестами.

clearAllMocks

Очищает историю вызовов, но сохраняет реализацию.

afterEach(() => {
  jest.clearAllMocks();
});

Используется, когда mock-реализация одинакова для всех тестов.

resetAllMocks

Полностью сбрасывает mocks к изначальному состоянию.

afterEach(() => {
  jest.resetAllMocks();
});

Важно: сбрасываются и реализации, заданные через mockImplementation.

restoreAllMocks

Возвращает оригинальные реализации для spies.

afterEach(() => {
  jest.restoreAllMocks();
});

Актуально только для jest.spyOn. Для jest.mock не влияет.

Очистка модульного кэша

Jest кэширует загруженные модули. Если mock зависит от состояния или конфигурации, требуется сброс кэша.

jest.resetModules();

После этого следующий require или import загрузит модуль заново.

Частый сценарий — тестирование разных конфигураций одного и того же модуля:

test('вариант A', () => {
  jest.resetModules();
  jest.doMock('../config', () => ({ mode: 'A' }));
  const module = require('../module');
});

test('вариант B', () => {
  jest.resetModules();
  jest.doMock('../config', () => ({ mode: 'B' }));
  const module = require('../module');
});

jest.isolateModules

Для локальной изоляции без глобального сброса используется jest.isolateModules.

jest.isolateModules(() => {
  jest.doMock('../config', () => ({ debug: true }));
  const module = require('../module');
});

Модули, загруженные внутри колбэка, не попадают в общий кэш.

doMock и динамическое мокирование

jest.doMock не поднимается вверх файла и позволяет управлять mock внутри теста.

jest.doMock('../featureFlag', () => ({
  enabled: true,
}));

Обязательное условие — импорт через require, а не import.

Очистка DOM и React Testing Library

React Testing Library автоматически вызывает cleanup после каждого теста (начиная с v9). Это удаляет компоненты из DOM и предотвращает утечки.

import { render } from '@testing-library/react';

Дополнительный вызов cleanup требуется только при отключённой автоматике или нестандартной конфигурации.

Важно различать:

  • очистку DOM (RTL)
  • очистку mocks (Jest)

Это независимые механизмы.

Таймеры и их очистка

При использовании jest.useFakeTimers() состояние таймеров также требует сброса.

afterEach(() => {
  jest.useRealTimers();
});

Иначе фейковые таймеры могут повлиять на последующие тесты, особенно при асинхронном рендеринге.

Типичные ошибки при работе с mocks

Глобальный mock без очистки Приводит к зависимостям между тестами и нестабильным результатам.

Смешивание mockResolvedValue и mockImplementation без reset Поведение функции становится неочевидным.

Мокирование того, что тестируется Подмена внутренней логики компонента уничтожает смысл теста и снижает ценность проверки.

Использование jest.mock внутри describe Визуально выглядит корректно, но фактически применяется ко всему файлу.

Практика организации mocks

Распространённый подход — вынос фабрик mocks в отдельные файлы:

// __mocks__/userService.js
export const getUser = jest.fn();

Jest автоматически подхватывает такие модули при вызове jest.mock('../userService').

Преимущество — единое место для логики подмены и упрощённая очистка.

Связь mocks и философии RTL

React Testing Library ориентирована на тестирование поведения, а не реализации. Модульные mocks должны:

  • изолировать внешние эффекты
  • не вмешиваться во внутреннюю логику компонента
  • отражать реальные сценарии использования

Корректная очистка mocks поддерживает главный принцип: каждый тест — независимая, воспроизводимая единица.