Введение
Асинхронные компоненты — это те, которые взаимодействуют с сервером или внешними источниками данных через такие операции, как HTTP-запросы. В реальных проектах почти любое приложение так или иначе зависит от данных, которые загружаются с сервера. Представьте ситуацию: ваша пагинация ломается после отправки данных пользователем, но вы узнаете об этом только после деплоя. Весело? Не думаю.
Вместо того чтобы надеяться на удачу, тестирование асинхронных компонентов позволяет убедиться, что ваши функции:
- Правильно отправляют запросы к API.
- Корректно обрабатывают успешные ответы.
- Грамотно обрабатывают ошибки (например, отобразят "Ой, сервер снова упал").
Это повышает ваш уровень уверенности в том, что приложение работает как задумано в самых разных условиях.
Что мы будем тестировать?
Сегодня вы узнаете:
- Как работать с асинхронными операциями в тестах, используя
async/await. - Как тестировать загрузку данных и отображение состояния "загрузки" (loading).
- Как обрабатывать различные сценарии ответа от API (успех, ошибка, пустое состояние).
- Как создавать моки для запросов API.
- Как взаимодействовать с "живым" состоянием компонентов после загрузки данных.
Приготовились? Тогда поехали!
Пример приложения: отображение списка пользователей
Для практических примеров мы будем тестировать компонент, который загружает список пользователей с внешнего API и отображает их на странице. Если данных нет, он покажет "Нет пользователей". Если происходит ошибка, он отобразит сообщение с текстом ошибки.
Код компонента UserList
Вот как выглядит наш компонент:
import React, { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch users');
const data: User[] = await response.json();
setUsers(data);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p data-testid="loading">Загрузка...</p>;
if (error) return <p data-testid="error">{error}</p>;
if (users?.length === 0) return <p data-testid="no-users">Нет пользователей</p>;
return (
<ul data-testid="user-list">
{users?.map((user) => (
<li key={user.id} data-testid="user-item">{user.name}</li>
))}
</ul>
);
};
export default UserList;
Шаг 1: настраиваем моки для Fetch
В Jest моки — это спасение для асинхронного тестирования. Они позволяют симулировать запросы к API, чтобы тестировать вашу логику без фактического отправления запросов.
Давайте замокаем глобальный объект fetch. Вот так:
// __mocks__/globalFetch.ts
export const mockFetch = (response: any, shouldFail = false) => {
global.fetch = jest.fn(() =>
shouldFail
? Promise.reject(new Error('API is down'))
: Promise.resolve({
ok: true,
json: jest.fn().mockResolvedValue(response),
})
) as jest.Mock;
};
Эта функция позволяет подменить fetch на нашу собственную реализацию, которая возвращает либо успешный ответ, либо ошибку.
Шаг 2: написание тестов
Теперь перейдём к практической части — тестированию нашего компонента.
Тест 1: загрузка данных (loading state)
Мы начнём с проверки того, что компонент отображает состояние загрузки во время получения данных.
import { render, screen } from '@testing-library/react';
import UserList from './UserList';
test('отображение состояния загрузки', () => {
render(<UserList />);
const loadingText = screen.getByTestId('loading');
expect(loadingText).toBeInTheDocument();
});
Запускаем тест через Jest и видим, что тест успешно проходит. Ура! Наш компонент показывает загрузку.
Тест 2: успешная загрузка данных
Теперь добавим мокированный ответ API и протестируем успешный сценарий:
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
import { mockFetch } from '../__mocks__/globalFetch';
test('успешная загрузка данных и отображение списка пользователей', async () => {
const mockUsers = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
mockFetch(mockUsers);
render(<UserList />);
await waitFor(() => expect(screen.getByTestId('user-list')).toBeInTheDocument());
const userItems = screen.getAllByTestId('user-item');
expect(userItems).toHaveLength(2);
expect(userItems[0].textContent).toBe('Alice');
expect(userItems[1].textContent).toBe('Bob');
});
Тест 3: обработка ошибок
Теперь проверим поведение компонента в случае ошибки API:
test('отображение ошибки при неудачной загрузке', async () => {
mockFetch(null, true); // Симулируем ошибку
render(<UserList />);
await waitFor(() => expect(screen.getByTestId('error')).toBeInTheDocument());
const errorText = screen.getByTestId('error');
expect(errorText.textContent).toBe('API is down');
});
Тест 4: отображение пустого списка
И, наконец, проверка на случай, если список пользователей пуст.
test('отображение сообщения об отсутствии пользователей', async () => {
mockFetch([]);
render(<UserList />);
await waitFor(() => expect(screen.getByTestId('no-users')).toBeInTheDocument());
const noUsersText = screen.getByTestId('no-users');
expect(noUsersText.textContent).toBe('Нет пользователей');
});
Особенности и типичные ошибки
Вы уже знаете, что в асинхронных тестах есть свои подводные камни. Например, забудете добавить await — ожидайте «фантомные» ошибки. Кроме того, при работе с состояниями, завязанными на запросах, легко забыть проверить все ветви логики (успех, ошибка, пустое состояние). Следите, чтобы все случаи были покрыты.
Ещё одна популярная ошибка — не учитывать, что тестируемый компонент может вызываться несколько раз, например, в случае повторного запроса. Всегда проверяйте, что мокированные данные корректно сбрасываются между тестами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ