Мокирование модулей с require.cache

При тестировании компонентов на React иногда возникает необходимость контролировать поведение внешних модулей, особенно если они содержат сложную логику, сетевые запросы или состояния, которые трудно предсказать. Одним из способов добиться этого является прямое манипулирование require.cache в Node.js. Этот подход позволяет «сбрасывать» или заменять модули во время выполнения теста, что дает полный контроль над их поведением.

Принципы работы require.cache

Node.js кэширует загруженные модули в объекте require.cache. Ключами этого объекта являются абсолютные пути к файлам модулей, а значениями — сами объекты модулей. При повторном вызове require('module') Node возвращает кэшированную версию, вместо того чтобы загружать файл заново.

Ключевые моменты:

  • require.cache хранит объекты модулей, включающие exports и внутренние свойства модуля.
  • Прямое удаление записи из кэша позволяет Node заново загрузить модуль при следующем require.
  • Это особенно полезно при тестировании модулей с синглтонами, состоянием или побочными эффектами, которые нужно перезапустить для каждого теста.

Удаление модуля из кэша

Для того чтобы сбросить модуль, необходимо получить его абсолютный путь и удалить из require.cache:

const path = require('path');

function clearModuleCache(moduleName) {
  const modulePath = require.resolve(moduleName);
  delete require.cache[modulePath];
}

После вызова clearModuleCache('./myModule') следующий require('./myModule') создаст новый экземпляр модуля.

Применение этого подхода в тестах позволяет:

  • Изолировать тесты, чтобы изменения в состоянии модуля одного теста не влияли на другие.
  • Подставлять моки вместо оригинальной реализации.

Замена модуля на мок

Можно не только удалить модуль из кэша, но и сразу подменить его экспорт на мок-объект:

function mockModule(moduleName, mockImplementation) {
  const modulePath = require.resolve(moduleName);
  require.cache[modulePath] = {
    id: modulePath,
    filename: modulePath,
    loaded: true,
    exports: mockImplementation
  };
}

Пример использования:

mockModule('./api', {
  fetchData: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
});

const api = require('./api');

api.fetchData().then(data => console.log(data));
// { id: 1, name: 'Test' }

Здесь api.fetchData больше не выполняет оригинальный сетевой запрос, а возвращает заранее заданный результат.

Комбинация с React Testing Library

При тестировании компонентов React, которые импортируют сторонние модули (например, API-клиенты, конфигурации или сервисы), важно гарантировать, что каждый тест использует чистый модуль.

Пример:

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

beforeEach(() => {
  // Сбрасываем кэш модуля перед каждым тестом
  clearModuleCache('./api');
});

test('компонент отображает данные из API', async () => {
  // Подменяем модуль на мок
  mockModule('./api', {
    fetchData: jest.fn().mockResolvedValue({ title: 'Hello World' })
  });

  const { default: MyComponent } = require('./MyComponent');
  
  render(<MyComponent />);
  
  const element = await screen.findByText('Hello World');
  expect(element).toBeInTheDocument();
});

Преимущества такого подхода:

  • Модуль ./api создается заново для каждого теста.
  • Нет необходимости в глобальном моке через jest.mock, что делает тесты более локализованными и предсказуемыми.
  • Позволяет комбинировать разные реализации модуля в разных тестах.

Ограничения и предостережения

  • Модули с круговыми зависимостями могут вести себя непредсказуемо после удаления из кэша.
  • Следует сбрасывать кэш только тестируемые модули, иначе есть риск повлиять на Node.js или сторонние библиотеки.
  • Частое удаление и подмена модулей может замедлить выполнение тестов, если модули тяжелые.

Рекомендации по использованию

  1. Использовать для модулей с состоянием — особенно синглтоны, кеши или сервисы.
  2. Комбинировать с jest.fn() и jest.spyOn() для контроля вызовов и проверок.
  3. Изолировать логику мокирования в отдельные функции, чтобы тесты оставались читаемыми.
  4. Не злоупотреблять: для большинства случаев проще использовать стандартный jest.mock, msw или dependency injection.

Манипулирование require.cache — мощный инструмент для глубокого контроля над модульной системой в тестах React. Он обеспечивает чистоту среды тестирования, возможность точного мокирования и помогает справляться с модулями, которые сложно тестировать стандартными методами.