В современных приложениях React часто возникает необходимость взаимодействовать с внешними API. Компоненты получают данные асинхронно и отображают их пользователю. Тестирование таких компонентов требует особого подхода, чтобы корректно проверять асинхронные операции и поведение интерфейса при разных сценариях ответа сервера.
Компоненты, делающие запросы к API, обычно используют хук
useEffect для выполнения запросов после монтирования.
Пример базового паттерна:
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error.message}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
Ключевые моменты:
useEffect выполняется после первого рендера.loading и error позволяют компоненту
корректно реагировать на состояние запроса.state с помощью
useState.Для тестирования компонентов важно изолировать их от реального API. Используются моки. Наиболее распространенные подходы:
fetchglobal.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ id: 1, name: 'Alice' }])
})
);
msw (Mock Service
Worker)msw позволяет подменять запросы на уровне сети, что
делает тесты ближе к реальности без вызова настоящего сервера.
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'Alice' }]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
React Testing Library предоставляет утилиты для работы с асинхронным DOM:
findBy* – возвращает промис, который резолвится, когда
элемент появляется.waitFor – ожидает выполнение колбэка до успешного
завершения или таймаута.Пример теста компонента UserList:
import { render, screen } from '@testing-library/react';
import UserList from './UserList';
test('отображает список пользователей после загрузки', async () => {
render(<UserList />);
expect(screen.getByText(/Загрузка/i)).toBeInTheDocument();
const userItem = await screen.findByText('Alice');
expect(userItem).toBeInTheDocument();
});
Особенности:
findByText автоматически ждет появления элемента в
DOM.act вручную — RTL
оборачивает асинхронные действия.Тесты должны учитывать ошибки сети или некорректные ответы:
test('отображает сообщение об ошибке при неудачном запросе', async () => {
global.fetch.mockImplementationOnce(() =>
Promise.reject(new Error('Network Error'))
);
render(<UserList />);
const errorMessage = await screen.findByText(/Ошибка/i);
expect(errorMessage).toBeInTheDocument();
});
Подход позволяет гарантировать корректное поведение интерфейса при сбоях.
Для упрощения тестирования рекомендуется выносить логику запросов в отдельные функции или сервисы:
export const fetchUsers = async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Ошибка запроса');
return res.json();
};
Компонент становится проще:
useEffect(() => {
fetchUsers()
.then(setUsers)
.catch(setError)
.finally(() => setLoading(false));
}, []);
Преимущества:
Компоненты часто обновляют данные по действиям пользователя, например, при клике кнопки:
function RefreshButton({ onRefresh }) {
return <button onCl ick={onRefresh}>Обновить</button>;
}
function UserListWithRefresh() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const loadUsers = async () => {
setLoading(true);
const data = await fetchUsers();
setUsers(data);
setLoading(false);
};
useEffect(() => { loadUsers(); }, []);
return (
<>
<RefreshButton onRefr esh={loadUsers} />
{loading ? <p>Загрузка...</p> : (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)}
</>
);
}
Тест такого компонента:
test('обновляет список при нажатии кнопки', async () => {
render(<UserListWithRefresh />);
const button = screen.getByText('Обновить');
global.fetch.mockImplementationOnce(() =>
Promise.resolve({ json: () => Promise.resolve([{ id: 2, name: 'Bob' }]) })
);
button.click();
const newUser = await screen.findByText('Bob');
expect(newUser).toBeInTheDocument();
});
Особенности:
mockImplementationOnce позволяет
контролировать последовательные ответы API.findBy* и waitFor вместо
синхронных getBy* для асинхронного рендера.Эти подходы позволяют создавать надежные, масштабируемые тесты для компонентов, взаимодействующих с API, без зависимости от реальной сети и серверной инфраструктуры.