Собственные валидаторы форм

При работе с формами в React часто возникает необходимость не только проверить корректность рендеринга элементов, но и убедиться, что валидация данных работает корректно. Встроенные HTML-валидации или сторонние библиотеки покрывают базовые случаи, однако в сложных сценариях требуется реализовать собственные валидаторы, а затем протестировать их поведение.

Организация формы и валидатора

Для примера рассмотрим простую форму с полями email и пароль. Каждое поле имеет свой валидатор, реализованный как отдельная функция:

export const validateEmail = (email) => {
  if (!email) return "Email обязателен";
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!regex.test(email)) return "Некорректный email";
  return null;
};

export const validatePassword = (password) => {
  if (!password) return "Пароль обязателен";
  if (password.length < 6) return "Пароль слишком короткий";
  return null;
};

В компоненте формы можно объединить валидаторы с локальным состоянием:

import React, { useState } from 'react';
import { validateEmail, validatePassword } from './validators';

export const LoginForm = ({ onSubmit }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    const emailError = validateEmail(email);
    const passwordError = validatePassword(password);
    const newErrors = { email: emailError, password: passwordError };
    setErrors(newErrors);

    if (!emailError && !passwordError) {
      onSubmit({ email, password });
    }
  };

  return (
    <form onSub mit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="text"
          value={email}
          onCha nge={(e) => setEmail(e.target.value)}
        />
        {errors.email && <span role="alert">{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="password">Пароль</label>
        <input
          id="password"
          type="password"
          value={password}
          onCha nge={(e) => setPassword(e.target.value)}
        />
        {errors.password && <span role="alert">{errors.password}</span>}
      </div>

      <button type="submit">Войти</button>
    </form>
  );
};

Тестирование валидаторов через React Testing Library

Ключевые моменты при тестировании пользовательской валидации:

  1. Рендер формы Используется render из RTL для отображения компонента в тестовом окружении.

  2. Эмуляция пользовательских действий События change и click позволяют симулировать ввод данных и отправку формы.

  3. Проверка ошибок валидации Элементы с ошибками можно искать по роли alert или тексту ошибки.

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

describe('LoginForm', () => {
  test('показывает ошибки при пустых полях', () => {
    render(<LoginForm onSub mit={jest.fn()} />);
    
    fireEvent.click(screen.getByText('Войти'));
    
    expect(screen.getByText('Email обязателен')).toBeInTheDocument();
    expect(screen.getByText('Пароль обязателен')).toBeInTheDocument();
  });

  test('показывает ошибку при некорректном email', () => {
    render(<LoginForm onSub mit={jest.fn()} />);
    
    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: 'invalid-email' },
    });
    fireEvent.change(screen.getByLabelText('Пароль'), {
      target: { value: '123456' },
    });
    fireEvent.click(screen.getByText('Войти'));

    expect(screen.getByText('Некорректный email')).toBeInTheDocument();
    expect(screen.queryByText('Пароль слишком короткий')).not.toBeInTheDocument();
  });

  test('не вызывает onSubmit при ошибках', () => {
    const handleSubmit = jest.fn();
    render(<LoginForm onSub mit={handleSubmit} />);
    
    fireEvent.click(screen.getByText('Войти'));

    expect(handleSubmit).not.toHaveBeenCalled();
  });

  test('вызывает onSubmit при корректных данных', () => {
    const handleSubmit = jest.fn();
    render(<LoginForm onSub mit={handleSubmit} />);
    
    fireEvent.change(screen.getByLabelText('Email'), {
      target: { value: 'test@example.com' },
    });
    fireEvent.change(screen.getByLabelText('Пароль'), {
      target: { value: '123456' },
    });
    fireEvent.click(screen.getByText('Войти'));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: '123456',
    });
  });
});

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

  • Разделять тесты валидаторов и формы. Функции validateEmail и validatePassword можно тестировать отдельно от компонента, что ускоряет тесты и делает их более стабильными.

  • Использовать role="alert" для сообщений об ошибках. Это упрощает поиск элементов в тестах и повышает доступность формы.

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

  • Минимизировать тестирование внутреннего состояния. Тесты должны проверять пользовательский опыт — появление и исчезновение ошибок, вызов onSubmit — а не внутренние useState.

Тестирование сложных валидаторов

Если валидаторы зависят от асинхронных операций, например, проверки уникальности email через API, используется комбинация RTL с waitFor:

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

test('асинхронная проверка email', async () => {
  render(<LoginForm onSub mit={jest.fn()} />);

  fireEvent.change(screen.getByLabelText('Email'), {
    target: { value: 'taken@example.com' },
  });
  fireEvent.click(screen.getByText('Войти'));

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

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

Интеграция с форм-библиотеками

Собственные валидаторы легко интегрируются с библиотеками типа Formik или React Hook Form, что позволяет:

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

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


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