На собесе по тестам проверяют три вещи: что именно ты тестируешь, какими инструментами и умеешь ли писать тесты, которые не ломаются от рефакторинга.
Что тестировать
Тестируйте поведение, которое видит пользователь, а не детали реализации. Пользователь не знает про useState и имена классов: он видит текст, кнопки, поля и результат своих действий.
Плохой тест проверяет, что состояние isOpen стало true. Хороший: что после клика по кнопке «Меню» появился список с пунктами. Первый сломается при переходе на useReducer, второй нет.
Популярная модель «кубок тестирования»: основа это статический анализ (TypeScript, ESLint), дальше немного юнит-тестов чистой логики, больше всего интеграционных тестов компонентов вместе с детьми, и несколько E2E на ключевые сценарии.
Инструменты
- Vitest (или Jest): раннер тестов,
expect, моки. В новых проектах на Vite чаще Vitest. Окружение DOM даётjsdomилиhappy-dom. - React Testing Library (RTL): рендерит компонент и ищет элементы так, как это делал бы пользователь.
- @testing-library/user-event: реалистичные действия пользователя.
- @testing-library/jest-dom: матчеры
toBeInTheDocument,toBeDisabled,toHaveTextContent(работают и в Vitest). - MSW: перехват запросов на уровне сети, чтобы не мокать
fetchруками. - Playwright: E2E в настоящих браузерах.
react-test-renderer в React 19 объявлен устаревшим, Enzyme не поддерживает современный React. Новые тесты на них не пишут.
Первый тест
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('добавляет задачу', async () => {
const user = userEvent.setup();
render(<TodoApp />);
await user.type(screen.getByRole('textbox', { name: 'Новая задача' }), 'Купить хлеб');
await user.click(screen.getByRole('button', { name: 'Добавить' }));
expect(screen.getByRole('listitem')).toHaveTextContent('Купить хлеб');
});
Методы user-event асинхронные, их нужно ждать через await.
Запросы: get, query, find
| Префикс | Нет элемента | Несколько | Когда |
|---|---|---|---|
getBy | бросает ошибку | ошибка | элемент должен быть сейчас |
queryBy | возвращает null | ошибка | проверить, что элемента нет |
findBy | промис отклоняется после таймаута | ошибка | элемент появится асинхронно |
У каждого есть вариант All для списков. findBy это waitFor плюс getBy, таймаут по умолчанию 1000 мс.
expect(screen.queryByText('Ошибка')).not.toBeInTheDocument();
expect(await screen.findByText('Готово')).toBeInTheDocument();
Приоритет запросов
getByRoleсname: так элементы видят скринридеры. Заодно проверяется доступность.getByLabelTextдля полей формы.getByPlaceholderText,getByText,getByDisplayValue.getByAltText,getByTitle.getByTestIdв последнюю очередь, когда иначе никак.
Если элемент не находится по роли, часто это сигнал, что разметка недоступна (например, div вместо button).
Асинхронность и act
RTL оборачивает render и события в act, поэтому вручную его почти не вызывают. Предупреждение «not wrapped in act» обычно значит, что тест закончился, а компонент ещё обновляется: нужно дождаться результата через findBy или waitFor.
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
В waitFor кладут только проверку: колбэк вызывается много раз, побочные действия там выполнятся многократно.
В React 19 act импортируют из react, старый путь react-dom/test-utils удалён.
Сеть и хуки
Запросы мокают через MSW: обработчики описывают ответы API, компонент делает настоящий fetch. Тест не знает, каким клиентом пользуется код, и переживёт замену axios на fetch.
Кастомный хук тестируют через renderHook из @testing-library/react, но если хук используется в одном компоненте, проще проверить компонент.
const { result } = renderHook(() => useCounter());
act(() => result.current.inc());
expect(result.current.count).toBe(1);
Таймеры
Для debounce и таймеров включают фейковые часы (vi.useFakeTimers()). user-event нужно сообщить о них: userEvent.setup({ advanceTimers: vi.advanceTimersByTime }), иначе действия зависнут.
Частые ошибки
- Проверка внутреннего состояния и вызова приватных функций.
getByдля проверки отсутствия (он бросит ошибку раньшеexpect).- Забытый
awaitпередuser.clickилиfindBy. - Повсеместный
data-testidвместо ролей. - Снапшоты огромных деревьев: их обновляют не глядя, и они ничего не ловят.
Что спросят на собесе
- Что тестировать в компоненте и чего не тестировать?
- Чем отличаются
getBy,queryByиfindBy? - Как замокать запросы к API в тестах компонентов?
- Почему RTL советует искать по роли?
- Как тестировать асинхронный код и что значит предупреждение про
act?