Интеграционные тесты

Интеграционные тесты в контексте форм, построенных на связке React Hook Form и YupResolver, проверяют взаимодействие нескольких уровней системы: UI-компонентов, схем валидации Yup, логики резолвера и поведения формы в реальном сценарии ввода данных. Основная цель таких тестов заключается не в проверке отдельных функций, а в подтверждении корректной работы всей цепочки валидации при изменении состояния формы.

Роль YupResolver в цепочке валидации

YupResolver выступает промежуточным слоем между схемой валидации Yup и системой управления формой React Hook Form. Он преобразует результат выполнения схемы в формат, который понимает форма:

  • принимает данные формы
  • передаёт их в Yup-схему
  • обрабатывает результат валидации
  • возвращает ошибки в структуре, ожидаемой React Hook Form

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

Базовая структура тестируемой формы

Типичный компонент формы с использованием YupResolver строится следующим образом:

  • React Hook Form управляет состоянием
  • Yup задаёт правила валидации
  • 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). Основной принцип заключается в симуляции пользовательского поведения:

  • ввод значений в поля
  • отправка формы
  • проверка появления ошибок
  • проверка вызова обработчика submit

Ключевым отличием от 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 через submit
  • корректное преобразование ошибок
  • интеграция с состоянием React Hook Form

Тестирование частичной валидации

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.

Асинхронная валидация и side effects

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();
});

В таких сценариях важно учитывать:

  • асинхронность Yup-схемы
  • корректное ожидание обновления состояния React Hook Form
  • синхронизацию моков fetch с жизненным циклом теста

Проверка трансформации данных через Yup

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

В проектах с TypeScript схема Yup часто используется для вывода типов формы. Хотя типизация не является частью runtime-тестов, интеграционные тесты косвенно подтверждают её корректность.

type FormValues = yup.InferType<typeof schema>;

Ошибки интеграции часто проявляются в:

  • несоответствии типов и реальных значений
  • неправильной трансформации в resolver
  • потере полей при касте данных

Поведение при динамических схемах

Некоторые формы используют динамическую схему в зависимости от состояния UI.

const schema = (isCompany) =>
  yup.object({
    email: yup.string().required(),
    companyName: isCompany ? yup.string().required() : yup.string().notRequired(),
  });

Интеграционные тесты должны проверять переключение логики:

  • изменение схемы
  • переинициализация resolver
  • обновление ошибок
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:

  • задержка обновления errors
  • несвоевременный ререндер
  • конфликт между resolver и manual validation

Для стабилизации используются:

  • findBy запросы React Testing Library
  • ожидание act
  • контроль debounce при валидации

Проверка reset и defaultValues

Reset формы должен корректно взаимодействовать с 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.

Масштабирование интеграционных сценариев

При росте формы интеграционные тесты начинают охватывать:

  • зависимости между полями
  • условную валидацию
  • массивы значений (field arrays)
  • вложенные структуры объектов

YupResolver в таких сценариях становится центральным узлом обработки, и тесты должны фиксировать:

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