Моки и стабы

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


Изоляция Yup-схемы через моки

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

Типичная стратегия заключается в замене реальной схемы на упрощённую реализацию:

const mockSchema = {
  validate: jest.fn(),
};

В контексте YupResolver важно, что схема должна иметь метод validate. При этом поведение метода полностью контролируется тестом:

mockSchema.validate.mockResolvedValue({
  name: "valid",
});

или в случае ошибки:

mockSchema.validate.mockRejectedValue({
  inner: [
    {
      path: "name",
      message: "Обязательное поле",
    },
  ],
});

Такой подход позволяет тестировать только логику резолвера без влияния реальной Yup-валидации.


Стабирование поведения YupResolver

YupResolver сам по себе является функцией-обёрткой, возвращающей асинхронный резолвер. В тестовой среде часто требуется стабировать результат его работы, чтобы исключить зависимость от внутренней реализации Yup.

import { yupResolver } from "@hookform/resolvers/yup";

jest.mock("@hookform/resolvers/yup", () => ({
  yupResolver: jest.fn(),
}));

После этого поведение резолвера задаётся явно:

yupResolver.mockReturnValue(async (values) => {
  if (values.name) {
    return {
      values,
      errors: {},
    };
  }

  return {
    values: {},
    errors: {
      name: {
        type: "required",
        message: "Обязательное поле",
      },
    },
  };
});

Такой стаб позволяет проверять реакцию формы на результат резолвера без выполнения реальной схемы.


Моки схемы Yup с кастомной логикой

При наличии сложной схемы, содержащей трансформации и кастомные проверки, мокирование может включать эмуляцию поведения cast, validateSync и isValid.

const mockSchema = {
  validate: jest.fn(),
  validateSync: jest.fn(),
  isValid: jest.fn(),
  cast: jest.fn(),
};

Пример поведения:

mockSchema.isValid.mockResolvedValue(true);
mockSchema.validateSync.mockReturnValue({
  email: "test@mail.com",
});

Подобная структура позволяет моделировать различные сценарии:

  • корректные данные
  • частично валидные данные
  • ошибки формата
  • исключения асинхронной валидации

Использование стабов для имитации ошибок Yup

Yup возвращает ошибки в виде объекта ValidationError, содержащего массив inner. При тестировании резолвера этот формат часто имитируется вручную.

const validationError = {
  inner: [
    {
      path: "email",
      message: "Некорректный email",
    },
    {
      path: "password",
      message: "Слишком короткий пароль",
    },
  ],
};

Затем стабируется отклонение промиса:

mockSchema.validate.mockRejectedValue(validationError);

Резолвер в таком случае должен трансформировать структуру в формат ошибок формы:

{
  email: { message: "Некорректный email" },
  password: { message: "Слишком короткий пароль" }
}

Частичная подмена логики YupResolver

В некоторых сценариях требуется тестировать не сам YupResolver, а слой над ним. Тогда резолвер полностью стабируется как функция:

const resolverMock = jest.fn();

Поведение задаётся напрямую:

resolverMock.mockImplementation(async (data) => {
  if (data.email === "ok@mail.com") {
    return {
      values: data,
      errors: {},
    };
  }

  return {
    values: {},
    errors: {
      email: {
        message: "Ошибка",
      },
    },
  };
});

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


Моки асинхронной валидации

Асинхронные проверки (например, уникальность email) требуют отдельного моделирования. Вместо реальных запросов используются стаб-функции:

const checkEmail = jest.fn();

Сценарии:

checkEmail.mockResolvedValue(true);
checkEmail.mockResolvedValue(false);

или имитация ошибки сети:

checkEmail.mockRejectedValue(new Error("Network error"));

В схеме Yup это обычно выглядит как:

Yup.string().test(
  "check-email",
  "Email уже существует",
  async (value) => {
    return await checkEmail(value);
  }
);

Комбинирование моков схемы и резолвера

Наиболее изолированный подход достигается при одновременной подмене схемы и резолвера. Схема возвращает фиксированный результат, а резолвер преобразует его в формат формы.

mockSchema.validate.mockResolvedValue({
  username: "user",
});

yupResolver.mockReturnValue(async () => ({
  values: { username: "user" },
  errors: {},
}));

Такое разделение позволяет тестировать отдельные уровни ответственности без пересечений логики.


Поведение при синхронных исключениях

Синхронная ошибка схемы может возникать при использовании validateSync. В тестах она моделируется через выброс исключения:

mockSchema.validateSync.mockImplementation(() => {
  throw {
    inner: [
      {
        path: "age",
        message: "Некорректный возраст",
      },
    ],
  };
});

Резолвер в таком случае обязан обработать исключение и преобразовать его в структуру ошибок формы.


Подмена зависимостей Yup через автоматические моки

При использовании Jest возможно автоматическое мокирование модуля Yup:

jest.mock("yup", () => ({
  object: jest.fn(),
  string: jest.fn(),
  number: jest.fn(),
}));

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


Контроль гранулярности моков

Уровень мокирования определяется задачей теста:

  • мок только validate — проверка обработки ошибок резолвером
  • мок всей схемы — тестирование логики формы
  • мок YupResolver — тестирование UI без валидации
  • мок асинхронных функций — изоляция внешних зависимостей

Гранулярность влияет на стабильность тестов и их устойчивость к изменениям в библиотеке Yup или реализации резолвера.