В React Router версии 6.4+ появились новые концепции loader и action, которые отвечают за загрузку данных и обработку действий формы при навигации. Тестирование этих функций требует понимания их роли в жизненном цикле маршрута и подходов к асинхронной обработке.
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:
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());
import { userLoader } from './userLoader';
test('loader возвращает данные пользователя', async () => {
const params = { userId: '1' };
const result = await userLoader({ params });
expect(result).toEqual({ id: '1', name: 'Test User' });
});
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 обрабатывает запросы формы или действия
пользователя на маршруте. Она получает объект 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:
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' });
});
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');
});
async/await и проверку ошибок через .rejects
для промисов.jest.spyOn(global, 'fetch') или библиотеку
msw.Помимо изолированных 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 обеспечивает стабильность маршрутов, предсказуемую загрузку данных и надёжную обработку пользовательских действий, особенно в больших приложениях с динамическими формами и асинхронной логикой.