Тестирование multi-step форм

Многошаговые формы (multi-step forms) часто встречаются в веб-приложениях для постепенного ввода больших объёмов данных. Тестирование таких форм требует особого внимания к состоянию, навигации между шагами и валидации пользовательского ввода. React Testing Library (RTL) предоставляет инструменты для взаимодействия с компонентами так, как это делает пользователь, что делает её идеальным инструментом для таких сценариев.


Выбор подхода к тестированию

В многошаговых формах можно выделить несколько ключевых аспектов для тестирования:

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

  2. Валидация и обработка ошибок Проверка реакций формы на неверный ввод, корректное отображение сообщений об ошибках и блокировку перехода на следующий шаг.

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

  4. Финальная отправка данных Проверка корректности агрегированных данных и их передачи на сервер или в хранилище состояния.


Рендеринг и навигация между шагами

В React Testing Library для рендеринга компонента используется функция render(). Для многошаговой формы важно протестировать видимость активного шага:

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

test('первый шаг отображается при рендере', () => {
  render(<MultiStepForm />);
  expect(screen.getByText(/Шаг 1: личные данные/i)).toBeInTheDocument();
});

test('переход ко второму шагу по кнопке "Далее"', () => {
  render(<MultiStepForm />);
  fireEvent.click(screen.getByText(/Далее/i));
  expect(screen.getByText(/Шаг 2: адрес/i)).toBeInTheDocument();
});

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


Валидация и обработка ошибок

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

  • Кнопка «Далее» блокируется при пустых обязательных полях.
  • Сообщения об ошибках отображаются корректно.

Пример:

test('показывает сообщение об ошибке при пустом имени', () => {
  render(<MultiStepForm />);
  fireEvent.click(screen.getByText(/Далее/i));
  expect(screen.getByText(/Имя обязательно/i)).toBeInTheDocument();
});

Для симуляции пользовательского ввода используется fireEvent.change или userEvent.type из пакета @testing-library/user-event:

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

test('позволяет заполнить имя и перейти к следующему шагу', async () => {
  render(<MultiStepForm />);
  await userEvent.type(screen.getByLabelText(/Имя/i), 'Иван');
  fireEvent.click(screen.getByText(/Далее/i));
  expect(screen.getByText(/Шаг 2: адрес/i)).toBeInTheDocument();
});

Сохранение состояния между шагами

Для многошаговых форм критично, чтобы введённые данные сохранялись при переходе назад и вперёд. Тесты должны проверять:

  • Наличие ранее введённых значений при возвращении к предыдущему шагу.
  • Корректную передачу данных на последнем шаге.

Пример:

test('сохраняет данные при переходе назад', async () => {
  render(<MultiStepForm />);
  await userEvent.type(screen.getByLabelText(/Имя/i), 'Иван');
  fireEvent.click(screen.getByText(/Далее/i));
  fireEvent.click(screen.getByText(/Назад/i));
  expect(screen.getByDisplayValue('Иван')).toBeInTheDocument();
});

Финальная отправка данных

После прохождения всех шагов необходимо проверить, что форма корректно агрегирует данные и вызывает соответствующую функцию отправки (onSubmit):

test('отправляет все данные при завершении формы', async () => {
  const handleSubmit = jest.fn();
  render(<MultiStepForm onSub mit={handleSubmit} />);

  await userEvent.type(screen.getByLabelText(/Имя/i), 'Иван');
  fireEvent.click(screen.getByText(/Далее/i));
  await userEvent.type(screen.getByLabelText(/Адрес/i), 'Москва');
  fireEvent.click(screen.getByText(/Отправить/i));

  expect(handleSubmit).toHaveBeenCalledWith({
    name: 'Иван',
    address: 'Москва'
  });
});

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


Использование асинхронных действий

В многошаговых формах часто есть асинхронная валидация или загрузка данных (например, проверка email или получение списка городов). RTL предоставляет waitFor и асинхронные методы findBy* для корректного тестирования:

import { waitFor } from '@testing-library/react';

test('показывает сообщение о недоступном email', async () => {
  render(<MultiStepForm />);
  await userEvent.type(screen.getByLabelText(/Email/i), 'test@example.com');
  fireEvent.click(screen.getByText(/Далее/i));

  await waitFor(() => {
    expect(screen.getByText(/Email уже используется/i)).toBeInTheDocument();
  });
});

Асинхронные проверки позволяют имитировать реальные задержки и события, возникающие при взаимодействии с API.


Рекомендации по структуре тестов

  • Группировка тестов по шагам: describe('Шаг 1', () => { ... }) позволяет локализовать ошибки.
  • Повторное использование пользовательских действий: Создание вспомогательных функций fillStep1(), fillStep2() упрощает поддержку тестов.
  • Тестирование критических сценариев: Пустые поля, неправильные форматы, возврат к предыдущим шагам.
  • Использование screen вместо прямого обращения к рендеру: Подчеркивает ориентацию на пользовательский опыт.

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

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

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