Конвенции в проекте

В процессе работы с React Testing Library важно придерживаться определённых конвенций, чтобы код тестов был понятен, поддерживаем и совместим с остальной частью проекта. Соблюдение конвенций помогает минимизировать количество ошибок, улучшить читаемость тестов и упростить процесс совместной работы в команде. В этой части рассматриваются основные принципы и рекомендации по организации тестов в проекте с использованием React Testing Library.

Структура тестов

Каждый компонент и функция должны иметь свой собственный файл с тестами, который расположён в том же каталоге, что и сам компонент. Это помогает поддерживать структуру проекта в порядке и легко искать тесты, соответствующие конкретному файлу.

Пример:

/src
  /components
    /Button
      Button.js
      Button.test.js
    /Header
      Header.js
      Header.test.js

Файл с тестами должен иметь суффикс .test.js, чтобы было понятно, что это тесты для данного компонента. Это правило помогает быстро ориентироваться в проекте и избегать путаницы между исходным кодом и тестами.

Названия тестов

Названия тестов должны быть информативными и чётко описывать ожидаемое поведение компонента. Рекомендуется использовать формат it или test, что позволяет выразить намерения теста в контексте бизнес-логики, а не технических аспектов.

Пример правильного описания тестов:

it('должен рендерить кнопку с текстом "Отправить"', () => {
  render(<Button label="Отправить" />);
  expect(screen.getByRole('button')).toHaveTextContent('Отправить');
});

Использование фраз вроде “проверяет, что компонент”, “проверяет, что функция”, “отображает”, “вызывает” помогает чётко указывать, что именно тестируется. Это улучшает понимание теста не только для автора, но и для других членов команды, особенно если тесты будут проверяться в будущем.

Использование render и других утилит

render — это основная утилита, которая используется для рендеринга компонента в тестах. React Testing Library предоставляет функцию render, которая позволяет отобразить компонент в виртуальном DOM и предоставляет API для взаимодействия с ним, например, с помощью screen.

render(<Button label="Отправить" />);

Рекомендуется не использовать shallow render или прямое манипулирование внутренним состоянием компонента, поскольку это нарушает идеологию React Testing Library, которая ориентирована на тестирование компонентов с точки зрения пользователя. Тесты должны работать с компонентами так, как они будут использоваться в реальной жизни, через их публичные интерфейсы и визуальные элементы, а не через скрытые или внутренние детали реализации.

Действия с элементами

Для взаимодействия с компонентами важно использовать методы, предоставляемые React Testing Library. Это getBy, findBy, queryBy, а также их вариации, такие как getByText, getByRole, getByLabelText и так далее.

Пример:

// Поиск элемента по тексту
const button = screen.getByText('Отправить');

// Поиск элемента по роли
const button = screen.getByRole('button', { name: /отправить/i });

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

Ожидания и асинхронные действия

Если компонент зависит от асинхронных данных (например, делает запрос к серверу или ожидает загрузки данных), следует использовать асинхронные методы findBy и waitFor. Эти методы помогают обеспечить корректное выполнение теста, ожидая появления нужного элемента.

Пример:

it('должен отобразить список пользователей после загрузки данных', async () => {
  render(<UserList />);
  await waitFor(() => screen.getByText('Загрузка...'));
  const userItems = await screen.findAllByRole('listitem');
  expect(userItems).toHaveLength(3);
});

Использование асинхронных методов помогает гарантировать, что тесты будут работать корректно даже при задержках или асинхронных обновлениях состояния.

Чистота тестов

Тесты должны быть изолированными и независимыми. Это означает, что каждый тест должен работать независимо от других. Рекомендуется использовать метод beforeEach или afterEach для подготовки и очистки состояния перед или после каждого теста.

Пример:

beforeEach(() => {
  // Настройка состояния перед каждым тестом
});

afterEach(() => {
  // Очистка ресурсов после каждого теста
});

Эти методы помогают избежать влияния одного теста на другой и поддерживать чистоту и стабильность тестового набора.

Моки и шпионские функции

Использование моков и шпионов должно быть минимизировано, поскольку это нарушает принцип тестирования с точки зрения пользователя. Тем не менее, если компонент зависит от внешних сервисов или библиотек, моки могут быть полезными для тестирования.

Для создания моков рекомендуется использовать jest.fn() или jest.mock().

Пример:

jest.mock('axios');

it('должен загрузить данные с сервера', async () => {
  axios.get.mockResolvedValue({ data: ['user1', 'user2'] });
  render(<UserList />);
  await waitFor(() => screen.getByText('user1'));
  expect(screen.getByText('user2')).toBeInTheDocument();
});

Моки помогают создать искусственные зависимости, позволяя тестировать компонент в условиях, приближённых к реальным, без необходимости взаимодействовать с настоящими внешними сервисами.

Стилизация и классы

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

Тем не менее, если необходимо проверить стилизацию, можно использовать методы, такие как toHaveStyle.

Пример:

it('должен иметь правильный цвет фона', () => {
  render(<Button color="red" />);
  const button = screen.getByRole('button');
  expect(button).toHaveStyle('background-color: red');
});

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

Рефакторинг и поддержка тестов

Важно помнить, что тесты — это не статичный код, и они требуют того же подхода к рефакторингу, что и основной код проекта. Когда код компонента меняется, тесты тоже должны быть обновлены, чтобы отражать новые условия.

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

Пример:

function setup() {
  render(<Button label="Отправить" />);
  return screen.getByRole('button');
}

it('должен рендерить кнопку', () => {
  const button = setup();
  expect(button).toBeInTheDocument();
});

Такой подход помогает сократить дублирование кода и улучшить поддержку тестов в проекте.

Сводка

Конвенции при написании тестов с использованием React Testing Library позволяют создавать код, который легко поддерживать и расширять. Правильное оформление тестов, использование доступных методов взаимодействия с компонентами, соблюдение изоляции тестов и минимизация использования моков и шпионов — это основные принципы, которые должны быть частью каждого проекта. Чистые и понятные тесты не только повышают качество кода, но и помогают команде быстрее находить и исправлять ошибки.