Инструменты тестирования: Jest, Vitest, другие раннеры

React Testing Library (RTL) — это инструмент для тестирования компонентов React, ориентированный на поведение приложения с точки зрения пользователя, а не на внутренние детали реализации. Он обеспечивает легкость написания тестов, которые остаются актуальными при изменении структуры компонентов.

Установка и настройка

Для использования RTL необходима установка библиотеки вместе с тестовым раннером. Чаще всего применяют Jest или Vitest:

npm install --save-dev @testing-library/react @testing-library/jest-dom
npm install --save-dev jest

Для Vitest:

npm install --save-dev vitest @testing-library/react @testing-library/jest-dom

После установки важно подключить расширения матчеров (@testing-library/jest-dom) для удобной проверки DOM-элементов:

import '@testing-library/jest-dom';

Основные функции React Testing Library

RTL предоставляет набор функций для рендеринга компонентов и поиска элементов на основе их доступности:

  • render(component) — рендерит React-компонент в виртуальный DOM для тестирования.

  • screen — глобальный объект для поиска элементов в отрендеренном DOM.

  • Методы поиска:

    • getByText, getByRole, getByLabelText — выбрасывают ошибку, если элемент не найден.
    • queryByText, queryByRole — возвращают null, если элемент отсутствует.
    • findByText, findByRole — асинхронные, возвращают Promise, удобно для элементов, которые появляются с задержкой.

Пример рендеринга и проверки текста:

import { render, screen } from '@testing-library/react';
import MyButton from './MyButton';

test('кнопка отображает правильный текст', () => {
  render(<MyButton label="Нажми меня" />);
  expect(screen.getByText('Нажми меня')).toBeInTheDocument();
});

Взаимодействие с пользователем

Для имитации действий пользователя используют user-event, более реалистичный, чем fireEvent:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('увеличение счётчика по клику', async () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: 'Увеличить' });
  await userEvent.click(button);
  expect(screen.getByText('Счётчик: 1')).toBeInTheDocument();
});

Ключевой момент: user-event эмулирует реальные события браузера, включая задержки и фокус, что делает тесты ближе к реальному поведению пользователя.


Асинхронное тестирование

Многие компоненты работают с асинхронными данными, поэтому необходимо уметь тестировать отложенное появление элементов. Для этого применяют findBy* методы:

import { render, screen } from '@testing-library/react';
import AsyncComponent from './AsyncComponent';

test('загрузка данных отображает элемент', async () => {
  render(<AsyncComponent />);
  const item = await screen.findByText('Данные загружены');
  expect(item).toBeInTheDocument();
});

Также можно использовать waitFor, если нужно дождаться изменения состояния:

import { render, screen, waitFor } from '@testing-library/react';
import AsyncToggle from './AsyncToggle';

test('кнопка меняет текст после загрузки', async () => {
  render(<AsyncToggle />);
  await waitFor(() => expect(screen.getByText('Готово')).toBeInTheDocument());
});

Проверка состояния компонента

React Testing Library ориентирован на видимый результат для пользователя, поэтому тесты проверяют текст, роли, атрибуты, а не внутренние методы или стейт напрямую. Например:

expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.getByLabelText('Согласие')).toBeChecked();

Использование toHaveAttribute и toHaveValue позволяет проверять DOM-атрибуты:

expect(screen.getByRole('textbox')).toHaveValue('Привет');
expect(screen.getByRole('button')).toHaveAttribute('disabled');

Настройка Jest и Vitest для RTL

Jest:

  • Файл конфигурации jest.config.js:
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
  • В jest.setup.js подключение jest-dom:
import '@testing-library/jest-dom';

Vitest:

  • В vitest.config.js:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: './vitest.setup.js',
  },
});
  • В vitest.setup.js:
import '@testing-library/jest-dom';

Тестирование событий формы

RTL позволяет тестировать формы максимально приближенно к пользователю:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

test('ввод данных в форму и отправка', async () => {
  render(<LoginForm />);
  await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
  await userEvent.type(screen.getByLabelText('Пароль'), '123456');
  await userEvent.click(screen.getByRole('button', { name: 'Войти' }));
  expect(screen.getByText('Добро пожаловать')).toBeInTheDocument();
});

Отказ от тестирования деталей реализации

RTL не рекомендуется проверять внутренние методы или состояние компонента. Тестирование должно опираться на видимые изменения DOM и пользовательский опыт, что повышает устойчивость тестов к рефакторингу.

Пример неправильного подхода:

// Не стоит так тестировать
expect(component.state.count).toBe(1);

Правильный подход:

expect(screen.getByText('Счётчик: 1')).toBeInTheDocument();

Практические советы

  • Предпочтение screen над деструктурированным возвратом render — упрощает чтение тестов.
  • Использовать getByRole с опцией name для более точного поиска элементов.
  • Для асинхронных действий всегда использовать findBy* или waitFor.
  • Минимизировать тесты, зависящие от структуры DOM, чтобы облегчить рефакторинг.

React Testing Library вместе с Jest или Vitest формирует мощную экосистему для написания надежных, читаемых и поддерживаемых тестов, которые проверяют приложение с точки зрения конечного пользователя, а не внутренней реализации компонентов.