Глубокие моки сложных систем

Мокирование является ключевым инструментом при тестировании компонентов, особенно когда речь идёт о сложных системах с множественными зависимостями. В React Testing Library (RTL) акцент делается на тестировании компонентов в условиях, максимально приближенных к реальному поведению пользователя, поэтому моки используются не только для имитации функций, но и для управления сетевыми запросами, контекстами и глобальными состояниями.

Моки функций и обработчиков событий

Самый базовый уровень мокирования — это имитация функций и событий. В JavaScript чаще всего используют jest.fn() для создания функций-заглушек:

import { render, fireEvent } from '@testing-library/react';
import Button from './Button';

test('вызывает переданную функцию при клике', () => {
  const handleClick = jest.fn();
  const { getByText } = render(<Button onCl ick={handleClick}>Нажми меня</Button>);
  
  fireEvent.click(getByText('Нажми меня'));
  
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Ключевой момент: мок позволяет изолировать компонент от внешней логики, фиксируя лишь факт вызова функции и её параметры.

Моки асинхронных операций

Многие компоненты зависят от API-запросов. Использование моков здесь предотвращает обращение к реальному серверу и обеспечивает стабильность тестов. В RTL часто применяют jest.mock() для замены модулей:

import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserList from './UserList';

jest.mock('axios');

test('отображает список пользователей после загрузки', async () => {
  const users = [{ id: 1, name: 'Иван' }, { id: 2, name: 'Мария' }];
  axios.get.mockResolvedValue({ data: users });

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText('Иван')).toBeInTheDocument();
    expect(screen.getByText('Мария')).toBeInTheDocument();
  });
});

mockResolvedValue позволяет задать поведение промиса, что имитирует успешный ответ сервера. Для ошибок используется mockRejectedValue, что помогает тестировать обработку ошибок в компонентах.

Контексты и провайдеры

Современные приложения часто используют React Context для глобального состояния. В тестах важно мокировать контексты с необходимыми значениями, чтобы компонент корректно взаимодействовал с ними:

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

test('отображает правильную тему', () => {
  render(
    <ThemeContext.Provider value={{ theme: 'dark' }}>
      <ThemedButton />
    </ThemeContext.Provider>
  );

  expect(screen.getByRole('button')).toHaveClass('dark');
});

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

Моки сложных зависимостей и библиотек

Компоненты нередко зависят от сторонних библиотек: маршрутизаторов, менеджеров состояния, UI-фреймворков. В таких случаях используют комбинированные моки:

import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

const renderWithRouter = (ui, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route);
  return render(ui, { wrapper: BrowserRouter });
};

test('содержит ссылку на /about', () => {
  const { getByText } = renderWithRouter(<MyComponent />);
  expect(getByText('О нас').closest('a')).toHaveAttribute('href', '/about');
});

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

Тестирование эффектов и асинхронного состояния

Моки особенно важны при тестировании компонентов с эффектами (useEffect) и асинхронным состоянием. Основная техника — контролируемая имитация данных и времени:

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

jest.useFakeTimers();

test('обновляет счетчик каждую секунду', () => {
  render(<Timer />);
  
  act(() => {
    jest.advanceTimersByTime(1000);
  });

  expect(screen.getByText('1')).toBeInTheDocument();
  
  act(() => {
    jest.advanceTimersByTime(2000);
  });

  expect(screen.getByText('3')).toBeInTheDocument();
});

jest.useFakeTimers() и act() позволяют точно контролировать поток времени, что исключает зависимость тестов от реального времени.

Комбинированные стратегии мокирования

Сложные компоненты часто требуют сочетания нескольких техник: мок API-запросов, провайдеров, функций и таймеров одновременно. Такой подход позволяет создавать изоляцию каждого элемента системы, при этом сохраняя реалистичное поведение интерфейса:

jest.mock('./api');
import { fetchData } from './api';

fetchData.mockResolvedValue([{ id: 1, text: 'Пример' }]);

render(
  <ThemeContext.Provider value={{ theme: 'light' }}>
    <MyComponent />
  </ThemeContext.Provider>
);

// Проверка правильного отображения данных и темы
expect(screen.getByText('Пример')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveClass('light');

Главная цель — сфокусироваться на поведении компонента, а не на работе внешних систем, сохраняя тесты стабильными и быстрыми.

Практические советы

  • Всегда предпочитать моки на уровне модулей, а не прямое вмешательство в компонент.
  • Использовать waitFor и findBy для асинхронного тестирования, избегая искусственных задержек.
  • Комбинировать контекстные и функциональные моки, если компонент зависит от нескольких слоёв данных.
  • Для сложных цепочек промисов или колбеков создавать отдельные утилиты моков для читаемости и повторного использования.

Мокирование в React Testing Library — это не только инструмент изоляции, но и способ гарантировать корректное поведение компонентов в реальных сценариях без зависимости от внешних сервисов.