Тестирование loader и action

В React Router версии 6.4+ появились новые концепции loader и action, которые отвечают за загрузку данных и обработку действий формы при навигации. Тестирование этих функций требует понимания их роли в жизненном цикле маршрута и подходов к асинхронной обработке.

Loader: тестирование загрузки данных

Loader — это асинхронная функция, которая вызывается перед рендерингом компонента маршрута. Она возвращает данные, необходимые компоненту, или бросает ошибку, если данные получить не удалось.

Пример типичной функции loader:

export async function userLoader({ params }) {
  const response = await fetch(`/api/users/${params.userId}`);
  if (!response.ok) throw new Response("Not Found", { status: 404 });
  return response.json();
}

Пошаговый подход к тестированию loader:

  1. Мокаем API-запросы Для тестов необходимо подменить реальные запросы, чтобы они возвращали предсказуемые данные. Используются библиотеки вроде msw или jest-fetch-mock.
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users/:userId', (req, res, ctx) => {
    return res(ctx.json({ id: req.params.userId, name: 'Test User' }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
  1. Вызов loader напрямую В тесте loader вызывается как обычная функция с передачей mock-параметров.
import { userLoader } from './userLoader';

test('loader возвращает данные пользователя', async () => {
  const params = { userId: '1' };
  const result = await userLoader({ params });
  expect(result).toEqual({ id: '1', name: 'Test User' });
});
  1. Обработка ошибок Проверка правильной работы loader при ошибках сети или при отсутствии данных.
server.use(
  rest.get('/api/users/:userId', (req, res, ctx) => {
    return res(ctx.status(404));
  })
);

test('loader бросает ошибку при 404', async () => {
  const params = { userId: '999' };
  await expect(userLoader({ params })).rejects.toThrow();
});

Action: тестирование действий формы

Action обрабатывает запросы формы или действия пользователя на маршруте. Она получает объект request и параметры маршрута.

Пример action:

export async function userAction({ request, params }) {
  const formData = await request.formData();
  const name = formData.get('name');
  
  const response = await fetch(`/api/users/${params.userId}`, {
    method: 'POST',
    body: JSON.stringify({ name }),
  });

  if (!response.ok) throw new Error('Failed to update user');
  return response.json();
}

Методы тестирования action:

  1. Создание mock Request В тестах создаётся объект Request, имитирующий реальный запрос формы.
import { userAction } from './userAction';

test('action обновляет пользователя', async () => {
  const formData = new FormData();
  formData.append('name', 'Updated User');

  const request = new Request('/users/1', {
    method: 'POST',
    body: formData,
  });

  const result = await userAction({ request, params: { userId: '1' } });
  expect(result).toEqual({ id: '1', name: 'Updated User' });
});
  1. Обработка ошибок Проверка поведения при неправильных данных или сбое API.
server.use(
  rest.post('/api/users/:userId', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);

test('action выбрасывает ошибку при сбое сервера', async () => {
  const formData = new FormData();
  formData.append('name', 'Failing User');

  const request = new Request('/users/1', { method: 'POST', body: formData });
  await expect(userAction({ request, params: { userId: '1' } })).rejects.toThrow('Failed to update user');
});

Практические рекомендации

  • Изолированное тестирование: loader и action должны тестироваться независимо от компонентов, чтобы тесты были быстрыми и стабильными.
  • Асинхронность: всегда использовать async/await и проверку ошибок через .rejects для промисов.
  • Мокирование fetch/axios: критично для предсказуемого поведения. Можно использовать jest.spyOn(global, 'fetch') или библиотеку msw.
  • Тестирование ошибок маршрута: проверять как loader/action реагируют на несуществующие данные, сбои сети и некорректные формы.
  • Проверка форматов данных: loader должен возвращать данные в том виде, в котором компонент их ожидает, а action — корректные статусы ответа и JSON.

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

Помимо изолированных unit-тестов, важно проверять взаимодействие loader/action с компонентами через React Testing Library и MemoryRouter.

import { render, screen } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import UserPage, { userLoader } from './UserPage';

test('рендер страницы с данными loader', async () => {
  render(
    <MemoryRouter initialEntries={['/users/1']}>
      <Routes>
        <Route path="/users/:userId" element={<UserPage />} loader={userLoader} />
      </Routes>
    </MemoryRouter>
  );

  expect(await screen.findByText('Test User')).toBeInTheDocument();
});

Такое тестирование позволяет убедиться, что данные, возвращаемые loader, корректно отображаются в компоненте, а ошибки обрабатываются React Router через errorElement.


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