Понимание концепции unit-testing

Unit testing — это метод тестирования, при котором отдельные компоненты приложения проверяются изолированно, без влияния внешних зависимостей. В контексте React это означает тестирование отдельных компонентов, функций или хуков с целью убедиться, что они корректно обрабатывают входные данные и возвращают ожидаемый результат.

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


Настройка окружения

Для использования RTL необходимо установить несколько пакетов:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest
  • @testing-library/react — основной пакет для рендеринга компонентов.
  • @testing-library/jest-dom — набор дополнительных матчеров для проверки DOM-элементов.
  • jest — тестовый раннер и фреймворк.

Конфигурация jest может быть минимальной, так как большинство проектов на Create React App уже включают предустановленные настройки.


Рендеринг компонента

Основной метод RTL для unit-тестов — render(). Он принимает компонент и возвращает объект с утилитами для поиска элементов в DOM:

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

test('рендеринг кнопки с текстом', () => {
  render(<Button label="Нажми меня" />);
  const buttonElement = screen.getByText('Нажми меня');
  expect(buttonElement).toBeInTheDocument();
});

Ключевые моменты:

  • Использование screen вместо возвращаемого объекта позволяет писать более читаемые тесты.
  • getByText ищет элементы по текстовому содержимому, что имитирует взаимодействие пользователя.

Поиск элементов

RTL предоставляет несколько стратегий поиска элементов:

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

Пример с ролью:

render(<button>Отправить</button>);
const button = screen.getByRole('button', { name: /отправить/i });
expect(button).toBeEnabled();

Симуляция событий

Для проверки поведения компонентов важно имитировать действия пользователя. RTL использует fireEvent и userEvent (рекомендуется userEvent для более реалистичных взаимодействий).

import { fireEvent } 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: /увеличить/i });
  await userEvent.click(button);
  const counter = screen.getByText('1');
  expect(counter).toBeInTheDocument();
});

Отличие fireEvent и userEvent:

  • fireEvent — низкоуровневая симуляция событий, не учитывает асинхронность и взаимодействия браузера.
  • userEvent — высокоуровневая симуляция, имитирует поведение настоящего пользователя, включая ввод текста, клики и фокус.

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

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

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

test('загрузка пользователей', async () => {
  render(<UserList />);
  const users = await screen.findAllByRole('listitem');
  expect(users.length).toBeGreaterThan(0);
});

test('ожидание изменений DOM', async () => {
  render(<UserList />);
  await waitFor(() => expect(screen.getByText('Иван')).toBeInTheDocument());
});
  • waitFor позволяет дождаться изменения DOM, полезно при асинхронных рендерах.
  • findBy* автоматически ждёт появления элемента до таймаута по умолчанию.

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

RTL не предоставляет прямого доступа к внутреннему состоянию компонентов, что является преимуществом: тесты становятся независимыми от реализации. Проверка происходит через эффекты на DOM:

render(<Toggle />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
userEvent.click(checkbox);
expect(checkbox).toBeChecked();

Состояние проверяется через видимый результат — состояние чекбокса, изменение текста или класса.


Мокирование зависимостей

Unit-тесты должны быть изолированы, поэтому внешние зависимости, такие как API-запросы, компоненты третьих сторон или хуки, часто мокируются:

jest.mock('./api', () => ({
  fetchData: jest.fn(() => Promise.resolve(['Пользователь1', 'Пользователь2']))
}));
  • Мокирование позволяет контролировать результаты и тестировать только логику компонента.
  • Важно восстанавливать оригинальные функции после тестов с помощью jest.restoreAllMocks().

Организация тестов

Структура тестов имеет большое значение для поддерживаемости:

  • Каждый компонент — отдельный файл тестов (Component.test.js).
  • Использование describe для группировки связанных тестов:
describe('Button', () => {
  test('рендерится с правильным текстом', () => {...});
  test('вызывает onClick при клике', () => {...});
});
  • Чистка DOM после тестов происходит автоматически с RTL (@testing-library/react/cleanup-after-each встроено в современную версию), что предотвращает влияние предыдущих тестов.

Лучшие практики

  • Тестировать поведение, а не реализацию: проверять текст, роли, видимые изменения.
  • Использовать userEvent вместо fireEvent для имитации действий пользователя.
  • Избегать тестирования деталей JSX, классов или внутреннего состояния компонента.
  • Мокировать внешние зависимости, чтобы тесты оставались быстрыми и стабильными.
  • Писать тесты небольшими блоками, проверяя одно действие или эффект за раз.
  • Использовать асинхронные утилиты RTL для компонентов, работающих с API или таймерами.

Этот подход позволяет создавать надежные unit-тесты для React-компонентов, обеспечивая высокое качество приложения и минимизируя риски ошибок при изменении кода.