React-компоненты проверяют глазами пользователя, а не по внутренней структуре. Testing Library запрашивает элементы по роли и доступному имени, по тексту и подписи - так, как их находит человек и вспомогательные технологии, - и намеренно не даёт удобного способа искать по className или пути в DOM. Для запуска нужны jsdom как среда, @testing-library/react, jest-dom для матчеров и user-event для взаимодействия.
Типичный тест описывает поведение формы. Пользователь вводит неверный email, нажимает кнопку, и на экране появляется сообщение об ошибке. Запросы идут по приоритету: сначала роль плюс доступное имя (кнопка 'создать аккаунт'), затем подпись поля (getByLabelText), затем видимый текст и лишь в крайнем случае test id. Этот порядок не каприз: он держит тест привязанным к тому, что видит и слышит пользователь.
Важно использовать userEvent, а не низкоуровневый fireEvent. userEvent моделирует настоящее взаимодействие: фокус, нажатия клавиш, порядок событий - так, как это происходит в браузере. Поэтому его вызовы асинхронны, и перед ними ставят await, а сам объект создают через userEvent.setup() в начале теста. Это ближе к реальному пользователю, чем одиночный синтетический fireEvent.
Запросы делятся на три семейства по назначению. getBy синхронный и нужен для того, что уже на экране; findBy асинхронный и ждёт появления элемента - им проверяют результат после запроса или перерисовки; queryBy* возвращает null и служит для проверки отсутствия. Путаница между ними - частый источник и ложных падений, и пропущенных ожиданий: findBy там, где нужно дождаться, и queryBy там, где проверяют, что чего-то нет.
Кастомные хуки заслуживают отдельного разговора. Когда в хуке лежит переиспользуемая логика, тестировать его через игрушечный компонент-обёртку неудобно и косвенно. Testing Library даёт renderHook, который исполняет хук напрямую и возвращает его результат. Тривиальный хук-склейку по-прежнему проще проверить через компонент, который его использует, но нетривиальную логику разумнее протестировать у источника.
У renderHook есть свои правила. result.current - это ссылка на последнее закоммиченное значение, поэтому его перечитывают после каждого обновления, а не захватывают в переменную заранее: старая ссылка покажет устаревшее состояние. Обновления состояния оборачивают в act(), новые пропсы подают через rerender, а зависимости вроде контекста и провайдеров передают опцией wrapper. Асинхронный хук дожидают через waitFor.
Форма и хук вместе покрывают фронтенд-тесты: компонент проверяют через видимое поведение, переиспользуемую логику - через renderHook. Оба подхода избегают деталей реализации: не важно, какой useState внутри и как назван внутренний обработчик, - важно, что пользователь видит ошибку, а хук отдаёт правильное значение при заданных входах и обновлениях.
Типичные провалы тоже общие. Захватить result.current в переменную и проверять устаревшее значение; проигнорировать предупреждение о необёрнутом в act() обновлении, за которым прячется реальная гонка; проверять деталь реализации - конкретный вызов внутреннего сеттера вместо наблюдаемого результата хука. Держитесь контракта: что компонент показывает и что хук возвращает, а не как это устроено внутри.
npm i -D jsdom @testing-library/react \
@testing-library/jest-dom @testing-library/user-event
# test/setup.ts: import '@testing-library/jest-dom/vitest'
# vitest.config.ts -> test:
# environment: 'jsdom'
# setupFiles: ['./test/setup.ts']test('показывает ошибку при неверном email', async () => {
const user = userEvent.setup()
render(<Signup />)
await user.type(screen.getByLabelText(/email/i), 'wrong')
await user.click(screen.getByRole('button', { name: /создать аккаунт/i }))
expect(await screen.findByRole('alert')).toHaveTextContent(
'Введите корректный email',
)
})test('useDebouncedValue отдаёт значение после задержки', () => {
vi.useFakeTimers()
const { result, rerender } = renderHook(
({ value }) => useDebouncedValue(value, 300),
{ initialProps: { value: 'a' } },
)
rerender({ value: 'ab' })
act(() => vi.advanceTimersByTime(300))
expect(result.current).toBe('ab') // перечитываем, не захватываем
})