Рендеринг компонентов с маршрутами

При тестировании React-компонентов, использующих маршрутизацию через react-router-dom, возникает необходимость симулировать поведение маршрутизатора, чтобы компоненты корректно реагировали на изменение URL и навигацию. Простое использование render из React Testing Library без обертки маршрутизатора часто приводит к ошибкам, так как компоненты ожидают контекст роутера.

Использование MemoryRouter для тестов

MemoryRouter — это легковесный маршрутизатор, предназначенный для тестирования. Он хранит историю навигации в памяти и позволяет управлять начальными маршрутами:

import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import App from './App';

test('рендеринг компонента на определённом маршруте', () => {
  render(
    <MemoryRouter initialEntries={['/dashboard']}>
      <App />
    </MemoryRouter>
  );

  expect(screen.getByText(/Dashboard/i)).toBeInTheDocument();
});

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

  • initialEntries задаёт путь, на котором компонент будет изначально отрендерен.
  • Компоненты, использующие useLocation, useParams или useNavigate, корректно работают внутри MemoryRouter.

Тестирование навигации

Для проверки переходов между страницами можно манипулировать историей маршрутов. В MemoryRouter история доступна через initialEntries и initialIndex:

import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import userEvent from '@testing-library/user-event';

test('навигация по ссылкам', async () => {
  const history = createMemoryHistory({ initialEntries: ['/'] });
  
  render(
    <Router location={history.location} navigator={history}>
      <App />
    </Router>
  );

  const link = screen.getByText(/About/i);
  await userEvent.click(link);

  expect(history.location.pathname).toBe('/about');
  expect(screen.getByText(/About Page/i)).toBeInTheDocument();
});

Особенности подхода:

  • Используется компонент Router, который принимает объект history, созданный вручную.
  • Можно отслеживать изменения маршрута после действий пользователя, имитируя клики и переходы.

Тестирование динамических маршрутов

Компоненты с параметрами маршрута (/users/:id) требуют проверки правильной работы useParams. Для этого достаточно передать путь с конкретным значением:

test('рендеринг страницы пользователя с параметром id', () => {
  render(
    <MemoryRouter initialEntries={['/users/42']}>
      <App />
    </MemoryRouter>
  );

  expect(screen.getByText(/User ID: 42/i)).toBeInTheDocument();
});

Выводы по динамическим маршрутам:

  • MemoryRouter корректно парсит параметры и предоставляет их через useParams.
  • Возможность тестировать разные значения параметров позволяет проверять условную отрисовку компонентов.

Мокирование навигации и внешних эффектов

При тестировании компонентов, вызывающих navigate после событий или эффектов, удобно использовать мок-функции:

import { useNavigate } from 'react-router-dom';
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: jest.fn(),
}));

test('навигация после сабмита формы', () => {
  const mockNavigate = jest.fn();
  useNavigate.mockReturnValue(mockNavigate);

  render(<FormComponent />);
  userEvent.click(screen.getByText(/Submit/i));

  expect(mockNavigate).toHaveBeenCalledWith('/success');
});

Принципы мокирования:

  • useNavigate можно заменить на функцию-шпион, чтобы проверить, что переход был вызван с правильным маршрутом.
  • Такой подход не требует полноценного рендеринга MemoryRouter, если цель — только проверить вызов навигации.

Комбинирование тестов маршрутов и состояния

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

import { Provider } from 'react-redux';
import { store } from './store';

render(
  <Provider store={store}>
    <MemoryRouter initialEntries={['/profile']}>
      <App />
    </MemoryRouter>
  </Provider>
);

Замечания:

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

Асинхронные проверки при маршрутизации

Компоненты часто загружают данные после рендеринга. Для асинхронной навигации применяются утилиты findBy:

test('отображение данных после перехода', async () => {
  render(
    <MemoryRouter initialEntries={['/dashboard']}>
      <App />
    </MemoryRouter>
  );

  const userGreeting = await screen.findByText(/Welcome, John/i);
  expect(userGreeting).toBeInTheDocument();
});

Особенности асинхронного подхода:

  • findBy автоматически ждёт появления элемента в DOM, что идеально для данных, загружаемых через эффекты.
  • Можно комбинировать с userEvent для имитации навигации и проверки реакций компонентов на изменения состояния.

Итоговая структура тестов маршрутизированных компонентов

  1. Использовать MemoryRouter для базового рендеринга.
  2. Передавать initialEntries для задания начального пути.
  3. Для сложной навигации применять кастомный history через Router.
  4. Проверять параметры маршрута через useParams.
  5. Мокировать useNavigate для проверки вызовов навигации.
  6. Оборачивать компоненты в провайдеры состояния при необходимости.
  7. Применять асинхронные селекторы (findBy*) для компонентов с загрузкой данных.

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