В React useState является базовым хуком для управления
локальным состоянием компонентов. Его тестирование требует внимания к
изменениям состояния и корректной отрисовке UI в ответ на эти изменения.
React Testing Library (RTL) предоставляет удобные средства для работы с
компонентами, ориентируясь на пользовательское поведение вместо
внутренней реализации.
useState управляет значениями внутри функционального
компонента. Для тестирования важно проверять изменения DOM,
которые происходят в ответ на обновление состояния, а не само
состояние напрямую.
Пример простого компонента с useState:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span data-testid="count">{count}</span>
<button onCl ick={() => setCount(prev => prev + 1)}>Increment</button>
</div>
);
}
export default Counter;
Основная цель теста — убедиться, что при клике на кнопку значение счетчика увеличивается, а UI обновляется.
render
и screenRTL предоставляет функцию render для отрисовки
компонента и screen для поиска элементов по тексту, ролям
или тестовым идентификаторам.
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('увеличивает счетчик при клике', () => {
render(<Counter />);
const count = screen.getByTestId('count');
const button = screen.getByText('Increment');
expect(count.textContent).toBe('0');
fireEvent.click(button);
expect(count.textContent).toBe('1');
});
Ключевой момент: тест проверяет конечный результат в
DOM, а не сам useState. Это соответствует
принципам RTL: ориентироваться на поведение пользователя.
userEventfireEvent симулирует события низкого уровня, но для
более реалистичного поведения предпочтительнее использовать
userEvent, который имитирует действия пользователя:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('увеличивает счетчик при клике с userEvent', async () => {
render(<Counter />);
const count = screen.getByTestId('count');
const button = screen.getByText('Increment');
expect(count).toHaveTextContent('0');
await userEvent.click(button);
expect(count).toHaveTextContent('1');
});
userEvent позволяет проверять асинхронное поведение, а
также последовательность событий, которая ближе к реальному
использованию интерфейса.
Компоненты часто используют несколько useState или
сложные объекты в состоянии. В этом случае важно проверять все изменения
DOM, которые зависят от состояния:
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
setTodos([...todos, input]);
setInput('');
};
return (
<div>
<input
value={input}
onCha nge={e => setInput(e.target.value)}
placeholder="New todo"
/>
<button onCl ick={addTodo}>Add</button>
<ul>
{todos.map((todo, idx) => <li key={idx}>{todo}</li>)}
</ul>
</div>
);
}
Тестирование:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoList from './TodoList';
test('добавляет новый элемент в список', async () => {
render(<TodoList />);
const input = screen.getByPlaceholderText('New todo');
const button = screen.getByText('Add');
await userEvent.type(input, 'Buy milk');
await userEvent.click(button);
const todoItem = screen.getByText('Buy milk');
expect(todoItem).toBeInTheDocument();
expect(input).toHaveValue('');
});
Особое внимание уделяется очистке input после добавления и корректной отрисовке нового элемента. Это демонстрирует проверку нескольких состояний одновременно.
useState может быть частью компонентов с асинхронными
эффектами, например, при загрузке данных. В таких случаях используются
waitFor или findBy для ожидания обновленного
состояния в DOM:
function AsyncCounter() {
const [count, setCount] = useState(0);
const incrementAsync = () => {
setTimeout(() => setCount(prev => prev + 1), 500);
};
return (
<div>
<span>{count}</span>
<button onCl ick={incrementAsync}>Increment Async</button>
</div>
);
}
test('асинхронное увеличение счетчика', async () => {
render(<AsyncCounter />);
const button = screen.getByText('Increment Async');
await userEvent.click(button);
const updatedCount = await screen.findByText('1');
expect(updatedCount).toBeInTheDocument();
});
findBy автоматически ждет появления элемента в DOM, что
упрощает тестирование асинхронного поведения.
data-testid только там,
где нет возможности выбрать элемент по роли или тексту.waitFor,
findBy или userEvent.useState.Если компонент использует несколько хуков useState,
важно проверять их взаимодействие:
function MultiState() {
const [text, setText] = useState('');
const [enabled, setEnabled] = useState(false);
return (
<div>
<input value={text} onCha nge={e => setText(e.target.value)} />
<button onCl ick={() => setEnabled(prev => !prev)}>Toggle</button>
{enabled && <span>{text}</span>}
</div>
);
}
test('показывает текст только когда включено', async () => {
render(<MultiState />);
const input = screen.getByRole('textbox');
const toggle = screen.getByText('Toggle');
await userEvent.type(input, 'Hello');
expect(screen.queryByText('Hello')).not.toBeInTheDocument();
await userEvent.click(toggle);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
Тест показывает, как состояния могут зависеть друг от друга и как важно проверять условную отрисовку.
Использование React Testing Library позволяет создавать
тесты, фокусированные на поведении и пользовательском
опыте, при этом useState проверяется через эффекты
на интерфейс, что делает тесты устойчивыми к изменениям внутренней
реализации компонентов.