При работе с формами в 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>
);
};
Ключевые моменты при тестировании пользовательской валидации:
Рендер формы Используется render из
RTL для отображения компонента в тестовом окружении.
Эмуляция пользовательских действий События
change и click позволяют симулировать ввод
данных и отправку формы.
Проверка ошибок валидации Элементы с ошибками
можно искать по роли 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 обеспечивают детальное и надежное тестирование форм, позволяя выявлять ошибки валидации, асинхронные баги и некорректное взаимодействие пользователя с интерфейсом.