Примеры тестовых паттернов

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

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

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

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

test('должен рендерить компонент без ошибок', () => {
  render(<MyComponent />);
  const element = screen.getByText(/Hello, world!/i);
  expect(element).toBeInTheDocument();
});

Этот тест проверяет, что компонент MyComponent рендерится корректно и отображает текст “Hello, world!”.

Проверка наличия элементов

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

test('должен рендерить заголовок и кнопку', () => {
  render(<MyComponent />);
  expect(screen.getByRole('heading', { name: /Добро пожаловать/i })).toBeInTheDocument();
  expect(screen.getByRole('button', { name: /Нажми меня/i })).toBeInTheDocument();
});

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

Тестирование взаимодействий с пользователем

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

import userEvent from '@testing-library/user-event';

test('должен менять текст при нажатии кнопки', () => {
  render(<MyComponent />);
  const button = screen.getByRole('button', { name: /Нажми меня/i });
  userEvent.click(button);
  expect(screen.getByText(/Кнопка нажата/i)).toBeInTheDocument();
});

В этом примере с помощью userEvent.click симулируется клик по кнопке, а затем проверяется, изменился ли текст в компоненте.

Асинхронные тесты

При тестировании асинхронных операций важно правильно ожидать результаты. React Testing Library предоставляет функции, такие как findBy, чтобы обрабатывать асинхронные действия в тестах.

test('должен показать данные после загрузки', async () => {
  render(<MyComponent />);
  const data = await screen.findByText(/Загрузка завершена/i);
  expect(data).toBeInTheDocument();
});

В данном примере компонент загружает данные асинхронно. С помощью findByText ожидается, что текст “Загрузка завершена” появится в DOM, когда асинхронная операция завершится.

Мокирование функций и данных

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

jest.mock('./api', () => ({
  fetchData: jest.fn().mockResolvedValue('данные получены'),
}));

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

test('должен отобразить данные после загрузки', async () => {
  render(<MyComponent />);
  const data = await screen.findByText(/данные получены/i);
  expect(data).toBeInTheDocument();
});

Здесь мы мокируем функцию fetchData и заставляем её возвращать обещание с данными. Это позволяет тестировать компонент без реальной загрузки данных.

Тестирование эффектов

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

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

test('должен обновить текст через 2 секунды', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText(/Текст обновлён/i));
  expect(screen.getByText(/Текст обновлён/i)).toBeInTheDocument();
});

В данном случае waitFor используется для ожидания изменения текста после выполнения побочного эффекта в useEffect.

Тестирование контекста

Если компонент использует React Context для управления состоянием, можно протестировать его поведение, передавая значения через контекст.

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

test('должен отображать данные из контекста', () => {
  render(
    <MyContextProvider value="Тестовые данные">
      <MyComponent />
    </MyContextProvider>
  );
  expect(screen.getByText(/Тестовые данные/i)).toBeInTheDocument();
});

Здесь создаётся провайдер контекста с заданным значением, и компонент проверяет, отображаются ли данные из контекста.

Тестирование модальных окон

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

test('должен открывать и закрывать модальное окно', () => {
  render(<MyComponent />);
  const openButton = screen.getByRole('button', { name: /Открыть модалку/i });
  userEvent.click(openButton);
  
  const modal = screen.getByRole('dialog');
  expect(modal).toBeInTheDocument();
  
  const closeButton = screen.getByRole('button', { name: /Закрыть/i });
  userEvent.click(closeButton);
  expect(modal).not.toBeInTheDocument();
});

Здесь тестируется взаимодействие с кнопками, открывающими и закрывающими модальное окно. Важно проверить оба состояния: открытое и закрытое.

Тестирование динамических списков

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

test('должен корректно отображать список элементов', async () => {
  render(<MyComponent />);
  const listItem = await screen.findByText(/Первый элемент/i);
  expect(listItem).toBeInTheDocument();
});

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

Тестирование ошибок

Необходимо тестировать не только обычные сценарии, но и ошибочные. Например, если компонент должен отображать сообщение об ошибке, его нужно проверить.

test('должен показывать ошибку при неудачной загрузке данных', async () => {
  jest.mock('./api', () => ({
    fetchData: jest.fn().mockRejectedValue(new Error('Ошибка загрузки')),
  }));

  render(<MyComponent />);
  const errorMessage = await screen.findByText(/Ошибка загрузки/i);
  expect(errorMessage).toBeInTheDocument();
});

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

Параметризированные тесты

Параметризированные тесты полезны, когда необходимо проверить компонент с несколькими наборами входных данных. Для этого можно использовать функции вроде test.each.

test.each([
  ['один', 1],
  ['два', 2],
  ['три', 3],
])('должен показывать число %i для текста %s', (text, number) => {
  render(<MyComponent text={text} />);
  expect(screen.getByText(new RegExp(number, 'i'))).toBeInTheDocument();
});

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

Обработка внешних библиотек

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

test('должен анимировать элемент при рендере', () => {
  render(<MyComponent />);
  const animatedElement = screen.getByTestId('animated-element');
  expect(animatedElement).toHaveStyle('transform: scale(1)');
});

Этот тест проверяет, что элемент с анимацией отображается с начальным состоянием стиля.

Тестирование хуков

Для тестирования пользовательских хуков можно использовать