Связка React Hook Form и Yup часто используется для построения форм с декларативной валидацией. В этой архитектуре ответственность за управление состоянием формы лежит на React Hook Form, а правила проверки данных задаются через Yup-схемы. Связывающим звеном выступает YupResolver, который адаптирует Yup-схему к интерфейсу валидации React Hook Form.
При тестировании таких форм возникает несколько уровней проверки:
Каждый уровень требует собственного подхода и набора инструментов.
YupResolver выполняет роль адаптера: принимает Yup-схему и возвращает функцию-резолвер, совместимую с React Hook Form. При сабмите формы данные проходят через pipeline:
В тестах важно учитывать, что YupResolver не содержит бизнес-логики. Он лишь трансформирует результаты.
Перед интеграционным тестированием формы целесообразно изолированно проверять Yup-схемы. Это снижает сложность тестов компонентов.
Пример схемы:
import * as Yup from 'yup';
export const schema = Yup.object({
email: Yup.string().email('Invalid email').required('Required'),
password: Yup.string().min(8, 'Too short').required('Required'),
});
import { schema } from './schema';
describe('Yup schema validation', () => {
test('valid data passes', async () => {
const validData = {
email: 'test@mail.com',
password: '12345678',
};
await expect(schema.validate(validData)).resolves.toEqual(validData);
});
test('invalid email fails', async () => {
const invalidData = {
email: 'bad-email',
password: '12345678',
};
await expect(schema.validate(invalidData)).rejects.toThrow();
});
});
Преимущество такого подхода — исключение React-слоя и YupResolver из уравнения, что позволяет быстрее находить ошибки в бизнес-валидации.
При подключении resolver к React Hook Form схема начинает участвовать в жизненном цикле формы.
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { schema } from './schema';
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
const onSub mit = (data) => {
console.log(data);
};
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register('email')} />
<p>{errors.email?.message}</p>
<input type="password" {...register('password')} />
<p>{errors.password?.message}</p>
<button type="submit">Submit</button>
</form>
);
}
Интеграционные тесты проверяют взаимодействие между UI и валидатором. Используется React Hook Form в связке с YupResolver, поэтому тесты должны имитировать поведение пользователя.
Основной инструмент — React Testing Library.
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm validation', () => {
test('shows validation errors on submit', async () => {
render(<LoginForm />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'invalid' },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: '123' },
});
fireEvent.click(screen.getByText(/submit/i));
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
expect(await screen.findByText(/too short/i)).toBeInTheDocument();
});
});
В этом сценарии YupResolver вызывается автоматически через handleSubmit. Тест не взаимодействует с ним напрямую, что соответствует принципу чёрного ящика.
Yup поддерживает асинхронные проверки, например, проверку уникальности email.
const schema = Yup.object({
email: Yup.string()
.email()
.test('unique', 'Email taken', async (value) => {
const res = await fakeApiCheck(value);
return res === true;
}),
});
jest.mock('./api', () => ({
fakeApiCheck: jest.fn(),
}));
import { fakeApiCheck } from './api';
test('async validation fails when email is taken', async () => {
fakeApiCheck.mockResolvedValue(false);
await expect(schema.validate({
email: 'taken@mail.com',
})).rejects.toThrow('Email taken');
});
При интеграции с сервером важно отделять сетевую логику от валидации.
Типичная схема тестирования:
Пример:
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ exists: true }),
})
);
Такой подход позволяет стабилизировать тесты и избежать флаки-поведения.
Важный аспект — состояние формы:
const { result } = renderHook(() =>
useForm({
resolver: yupResolver(schema),
mode: 'onChange',
})
);
Тестирование состояния:
act(() => {
result.current.setValue('email', 'bad');
});
expect(result.current.formState.errors.email).toBeDefined();
React Hook Form поддерживает режимы:
YupResolver вызывается по-разному в зависимости от режима.
const { getByLabelText } = render(<LoginForm />);
fireEvent.change(getByLabelText(/email/i), {
target: { value: 'x' },
});
// ошибка появляется сразу
fireEvent.click(screen.getByText(/submit/i));
// ошибки появляются только после submit
Тесты должны учитывать этот контракт, иначе возможны ложные ожидания.
Иногда требуется исключить Yup и проверить только поведение формы.
jest.mock('@hookform/resolvers/yup', () => ({
yupResolver: () => () => ({ values: {}, errors: {} }),
}));
Такой подход используется редко, но полезен для:
Ошибка: попытка тестировать YupResolver напрямую вместо формы.
YupResolver не является бизнес-слоем, его поведение не должно проверяться отдельно в большинстве случаев.
Yup validate возвращает Promise, поэтому тесты без await приводят к ложноположительным результатам.
Смешивание уровней усложняет диагностику ошибок.
React Hook Form обновляет состояние асинхронно, поэтому обновления должны оборачиваться в act.
Yup возвращает ошибки в структуре:
{
"email": "Invalid email"
}
YupResolver трансформирует их в:
{
errors: {
email: {
message: 'Invalid email'
}
}
}
В тестах важно проверять именно финальную структуру React Hook Form, а не Yup.
Эффективная модель покрытия:
Такое распределение снижает зависимость от UI-тестов и ускоряет выполнение suite.
Yup поддерживает conditional schema:
Yup.string().when('role', {
is: 'admin',
then: (schema) => schema.required(),
});
Тестирование:
test('admin requires password', async () => {
await expect(
schema.validate({ role: 'admin' })
).rejects.toThrow();
});
React Hook Form сбрасывает состояние, но YupResolver не хранит state.
Тесты должны проверять:
act(() => {
result.current.reset();
});
expect(result.current.formState.errors).toEqual({});
Для сложных форм с десятками полей:
YupResolver в таких случаях остаётся неизменным посредником, не влияющим на стратегию тестирования, но определяющим формат ошибок и момент их появления.