Мокирование роутера в тестах

При тестировании приложений на базе TanStack Router критически важно изолировать поведение компонентов от реального роутинга. Мокирование роутера позволяет проверять логику навигации, рендеринга и взаимодействия с маршрутами без необходимости запускать полноценное приложение или эмулировать браузерную историю.

Основные принципы мокирования

  1. Изоляция зависимостей Компоненты не должны напрямую обращаться к реальному экземпляру роутера. Вместо этого создаётся мок-объект, который имитирует API роутера:

    const mockRouter = {
      navigate: jest.fn(),
      getCurrentRoute: jest.fn(),
      subscribe: jest.fn(),
    };

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

  2. Имитация маршрутов TanStack Router опирается на маршруты, которые определяют путь, параметры и логику загрузки данных. При тестировании можно создать фиктивный набор маршрутов:

    const routes = [
      { path: '/', component: HomeComponent },
      { path: '/profile/:id', component: ProfileComponent },
    ];
    const mockRoute = {
      pathname: '/profile/42',
      params: { id: '42' },
    };
    mockRouter.getCurrentRoute.mockReturnValue(mockRoute);

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

  3. Мокирование навигации Метод navigate используется для переходов между маршрутами. В тестах важно проверять, что вызовы происходят корректно:

    import { fireEvent } from '@testing-library/react';
    
    test('кнопка перенаправляет на профиль', () => {
      const { getByText } = render(<ProfileButton router={mockRouter} />);
      fireEvent.click(getByText('Перейти в профиль'));
      expect(mockRouter.navigate).toHaveBeenCalledWith('/profile/42');
    });
  4. Подписка на изменения маршрута Компоненты могут подписываться на изменения маршрута через router.subscribe. Мок позволяет имитировать события:

    const callback = jest.fn();
    mockRouter.subscribe.mockImplementation(cb => {
      cb(mockRoute);
      return () => {};
    });
    
    render(<SomeComponent router={mockRouter} />);
    expect(callback).toHaveBeenCalledWith(mockRoute);

Подходы к интеграционному тестированию

  • Использование MemoryRouter TanStack Router предоставляет возможность работы с “памятным” роутером (MemoryRouter), который не взаимодействует с URL браузера. Это облегчает тестирование навигации и рендеринга маршрутов в изолированной среде.

  • Мокирование хранилища состояний маршрута Для проверки редиректов и условного рендеринга можно создать фиктивное хранилище маршрутов и управлять им вручную:

    const routeStore = createRouteStore({ initialRoute: '/' });
    routeStore.setRoute('/profile/42');
    render(<App router={{ ...mockRouter, routeStore }} />);

Советы по организации тестов

  • Создавать отдельный файл с конфигурацией мок-роутера, чтобы повторно использовать его во всех тестах.
  • Использовать утилиты jest.fn() и jest.spyOn() для отслеживания вызовов методов и подписок.
  • Для сложных сценариев навигации создавать фабрики маршрутов, чтобы легко изменять набор параметров и проверять реакции компонентов.
  • Проверять не только вызовы навигации, но и корректность рендеринга компонентов в зависимости от текущего маршрута и параметров.

Частые ошибки при мокировании

  • Прямой импорт реального роутера вместо передачи через пропсы или контекст. Это делает тест зависимым от состояния браузера.
  • Игнорирование подписок: компоненты, использующие router.subscribe, могут вести себя некорректно без корректного мокирования.
  • Неучтённые параметры маршрута: при тестировании компонент, который использует useParams или route.params, нужно явно задавать значения в мок-объекте.

Пример комплексного теста

test('рендер компонента с параметрами маршрута и переход на другой маршрут', () => {
  const mockRouter = {
    navigate: jest.fn(),
    getCurrentRoute: jest.fn(() => ({ pathname: '/profile/42', params: { id: '42' } })),
    subscribe: jest.fn((cb) => { cb({ pathname: '/profile/42', params: { id: '42' } }); return () => {}; }),
  };

  const { getByText } = render(<ProfilePage router={mockRouter} />);
  expect(getByText('Профиль пользователя 42')).toBeInTheDocument();

  fireEvent.click(getByText('Редактировать'));
  expect(mockRouter.navigate).toHaveBeenCalledWith('/profile/42/edit');
});

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

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