Интеграционные тесты в контексте форм, построенных на связке React Hook Form и YupResolver, проверяют взаимодействие нескольких уровней системы: UI-компонентов, схем валидации Yup, логики резолвера и поведения формы в реальном сценарии ввода данных. Основная цель таких тестов заключается не в проверке отдельных функций, а в подтверждении корректной работы всей цепочки валидации при изменении состояния формы.
YupResolver выступает промежуточным слоем между схемой валидации Yup и системой управления формой React Hook Form. Он преобразует результат выполнения схемы в формат, который понимает форма:
Интеграционные тесты должны учитывать именно этот слой трансформации, поскольку ошибки часто возникают не в самой схеме, а в несоответствии формата данных между слоями.
Типичный компонент формы с использованием YupResolver строится следующим образом:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
export function LoginForm({ onSubmit }) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register("email")} placeholder="email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register("password")} placeholder="password" type="password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Интеграционные тесты для такого компонента должны проверять не только рендеринг, но и полное прохождение данных через resolver.
Интеграционные тесты форм с YupResolver обычно строятся на базе React Testing Library и Jest (или Vitest). Основной принцип заключается в симуляции пользовательского поведения:
Ключевым отличием от unit-тестов является отсутствие изоляции YupResolver. Он используется как часть реального потока данных.
Один из базовых сценариев — проверка отображения ошибок при некорректных данных.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
test("отображает ошибки валидации при пустой форме", async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSub mit={handleSubmit} />);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText(/email is a required field/i)).toBeInTheDocument();
expect(screen.getByText(/password is a required field/i)).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
});
В этом сценарии проверяется:
YupResolver может возвращать ошибки только для изменённых или затронутых полей. Интеграционные тесты должны учитывать поведение режима валидации, заданного в useForm.
Пример сценария с режимом onBlur:
useForm({
resolver: yupResolver(schema),
mode: "onBlur",
});
Тест:
test("валидирует поле при потере фокуса", async () => {
const user = userEvent.setup();
render(<LoginForm onSub mit={jest.fn()} />);
const emailInput = screen.getByPlaceholderText("email");
await user.type(emailInput, "invalid-email");
await user.tab();
expect(
await screen.findByText(/email must be a valid email/i)
).toBeInTheDocument();
});
Здесь проверяется взаимодействие событий DOM, React Hook Form и YupResolver.
Интеграционный тест должен подтверждать, что корректные данные проходят всю цепочку без ошибок.
test("успешная отправка формы при валидных данных", async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSub mit={handleSubmit} />);
await user.type(screen.getByPlaceholderText("email"), "test@mail.com");
await user.type(screen.getByPlaceholderText("password"), "strongpassword");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith(
{
email: "test@mail.com",
password: "strongpassword",
},
expect.anything()
);
});
Особенность интеграционного уровня заключается в том, что проверяется не только вызов функции, но и корректность данных, прошедших через YupResolver.
YupResolver поддерживает асинхронные схемы, включая проверки уникальности через API. В таких случаях интеграционные тесты требуют мокирования сетевых запросов.
const schema = yup.object({
email: yup
.string()
.email()
.test("unique", "email already exists", async (value) => {
const res = await fetch(`/api/check-email?email=${value}`);
const data = await res.json();
return data.available;
}),
});
Тестирование:
beforeEach(() => {
global.fetch = jest.fn();
});
test("показывает ошибку при занятом email", async () => {
global.fetch.mockResolvedValueOnce({
json: async () => ({ available: false }),
});
const user = userEvent.setup();
render(<LoginForm onSub mit={jest.fn()} />);
await user.type(screen.getByPlaceholderText("email"), "test@mail.com");
await user.type(screen.getByPlaceholderText("password"), "password123");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/email already exists/i)).toBeInTheDocument();
});
В таких сценариях важно учитывать:
YupResolver часто используется совместно с трансформацией данных
(transform, cast, default).
Интеграционные тесты должны фиксировать итоговую форму данных, а не
только ввод.
const schema = yup.object({
age: yup.number().transform((value) => Number(value)).required(),
});
Тест:
test("преобразует строковое значение в число", async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<AgeForm onSub mit={handleSubmit} />);
await user.type(screen.getByPlaceholderText("age"), "25");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith(
{ age: 25 },
expect.anything()
);
});
Проверяется не только факт отправки, но и корректность преобразования через Yup и resolver.
Yup может возвращать набор ошибок по нескольким полям. Интеграционные
тесты должны проверять агрегированное состояние
formState.errors.
test("отображает все ошибки сразу", async () => {
const user = userEvent.setup();
render(<LoginForm onSub mit={jest.fn()} />);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText(/email is a required field/i)).toBeInTheDocument();
expect(screen.getByText(/password is a required field/i)).toBeInTheDocument();
});
Здесь важна проверка того, что YupResolver не прерывает валидацию на первом найденном нарушении.
В проектах с TypeScript схема Yup часто используется для вывода типов формы. Хотя типизация не является частью runtime-тестов, интеграционные тесты косвенно подтверждают её корректность.
type FormValues = yup.InferType<typeof schema>;
Ошибки интеграции часто проявляются в:
Некоторые формы используют динамическую схему в зависимости от состояния UI.
const schema = (isCompany) =>
yup.object({
email: yup.string().required(),
companyName: isCompany ? yup.string().required() : yup.string().notRequired(),
});
Интеграционные тесты должны проверять переключение логики:
test("изменяет валидацию при переключении режима", async () => {
const user = userEvent.setup();
const { rerender } = render(
<Form isCompany={false} onSub mit={jest.fn()} />
);
await user.click(screen.getByRole("button", { name: /submit/i }));
rerender(<Form isCompany={true} onSub mit={jest.fn()} />);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(
await screen.findByText(/company name is a required field/i)
).toBeInTheDocument();
});
Интеграционные тесты часто выявляют проблемы, связанные с асинхронным обновлением состояния React Hook Form:
Для стабилизации используются:
findBy запросы React Testing LibraryactReset формы должен корректно взаимодействовать с YupResolver, особенно при наличии defaultValues.
test("сбрасывает форму к начальному состоянию", async () => {
const user = userEvent.setup();
render(<LoginForm onSub mit={jest.fn()} />);
await user.type(screen.getByPlaceholderText("email"), "test@mail.com");
await user.click(screen.getByRole("button", { name: /submit/i }));
await user.click(screen.getByRole("button", { name: /reset/i }));
expect(screen.getByPlaceholderText("email").value).toBe("");
});
Особое внимание уделяется тому, что reset не нарушает работу resolver при следующем submit.
При росте формы интеграционные тесты начинают охватывать:
YupResolver в таких сценариях становится центральным узлом обработки, и тесты должны фиксировать: