Test - создание собственных правил

Механизм test в Yup представляет собой основной инструмент расширения стандартной системы валидации. Он позволяет задавать произвольные правила проверки, выходящие за рамки встроенных методов (required, min, max, email и др.), и интегрировать бизнес-логику непосредственно в схему.

В контексте использования YupResolver (например, в связке с react-hook-form) такие правила становятся частью декларативной схемы, которая затем преобразуется резолвером в результат валидации формы.


Базовая структура метода .test()

Метод test добавляется к любому полю схемы Yup и принимает конфигурационный объект или набор аргументов:

Yup.string().test(
  name,
  message,
  testFunction
)

Где:

  • name — уникальное имя проверки
  • message — сообщение об ошибке
  • testFunction — функция, возвращающая true/false или ValidationError

Простейший пример:

import * as Yup from "yup";

const schema = Yup.object({
  username: Yup.string()
    .test(
      "no-spaces",
      "Имя пользователя не должно содержать пробелы",
      (value) => !/\s/.test(value || "")
    )
});

Контекст выполнения теста

Функция testFunction получает доступ к значению поля и дополнительному контексту через this (в обычной функции, не стрелочной):

Yup.string().test(
  "starts-with-a",
  "Значение должно начинаться с буквы A",
  function (value) {
    return value?.startsWith("A");
  }
);

Контекст this содержит:

  • this.parent — объект текущей формы
  • this.path — путь к полю
  • this.originalValue — исходное значение
  • this.options — параметры схемы

Использование this.parent позволяет реализовывать межполевую валидацию:

Yup.object({
  password: Yup.string().required(),
  confirmPassword: Yup.string().test(
    "match-password",
    "Пароли не совпадают",
    function (value) {
      return value === this.parent.password;
    }
  )
});

Работа с асинхронными тестами

test поддерживает асинхронную валидацию, что особенно важно при проверке уникальности данных через API.

const schema = Yup.object({
  email: Yup.string()
    .email()
    .test(
      "check-email-exists",
      "Email уже используется",
      async (value) => {
        if (!value) return true;

        const response = await fetch(`/api/check-email?email=${value}`);
        const data = await response.json();

        return data.available === true;
      }
    )
});

Особенность интеграции с YupResolver заключается в том, что резолвер корректно обрабатывает Promise и переводит результат в структуру ошибок формы.


Использование параметра context

При интеграции с YupResolver можно передавать внешний контекст в схему через context, что позволяет создавать динамические правила.

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

const schema = Yup.object({
  age: Yup.number().test(
    "min-age-by-country",
    "Возраст не соответствует требованиям страны",
    function (value) {
      const country = this.options.context?.country;

      if (country === "US") return value >= 21;
      if (country === "EU") return value >= 18;

      return true;
    }
  )
});

Использование:

resolver: yupResolver(schema, {
  context: { country: "US" }
});

Условные тесты и динамическая логика

test часто применяется для сложной логики, где стандартные методы Yup недостаточны.

const schema = Yup.object({
  isCompany: Yup.boolean(),
  companyName: Yup.string().test(
    "company-required",
    "Название компании обязательно",
    function (value) {
      const isCompany = this.parent.isCompany;

      if (!isCompany) return true;

      return typeof value === "string" && value.trim().length > 0;
    }
  )
});

Здесь поле становится обязательным только при определённом условии.


Повторное использование тестов

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

const noSpecialChars = (message) =>
  function (value) {
    return /^[a-zA-Z0-9]*$/.test(value || "");
  };

const schema = Yup.object({
  code: Yup.string().test(
    "no-special-chars",
    "Код содержит недопустимые символы",
    noSpecialChars()
  )
});

Более гибкий вариант с параметрами:

const minWords = (count) =>
  function (value) {
    if (!value) return true;
    return value.trim().split(/\s+/).length >= count;
  };

const schema = Yup.object({
  description: Yup.string().test(
    "min-words",
    "Недостаточно слов",
    minWords(10)
  )
});

Возврат кастомных ошибок через createError

Внутри test можно не только возвращать true/false, но и формировать детализированные ошибки:

Yup.string().test(
  "complex-validation",
  "Ошибка",
  function (value) {
    if (!value) {
      return this.createError({ message: "Поле обязательно" });
    }

    if (value.length < 5) {
      return this.createError({
        message: "Минимальная длина 5 символов"
      });
    }

    return true;
  }
);

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


Интеграция с YupResolver и реактивной формой

YupResolver преобразует Yup-схему в формат ошибок, понятный react-hook-form. При использовании test важно учитывать, что:

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

Пример подключения:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

const form = useForm({
  resolver: yupResolver(schema)
});

Композиция нескольких test в одном поле

Несколько тестов могут применяться последовательно:

Yup.string()
  .test("not-empty", "Пустое значение", (v) => !!v)
  .test("no-digits", "Не должно содержать цифры", (v) => !/\d/.test(v || ""))
  .test("min-length", "Минимум 3 символа", (v) => (v || "").length >= 3);

Каждый тест выполняется независимо, и первый неуспешный прерывает цепочку.


Типичные ошибки при создании тестов

Часто встречающиеся проблемы:

  • использование стрелочной функции при необходимости this
  • отсутствие обработки undefined или null
  • возврат неявных значений вместо true/false
  • блокирующие асинхронные запросы без кеширования
  • дублирование логики в разных test

Пример некорректного подхода:

Yup.string().test("bad", "Ошибка", (value) => {
  this.parent; // недоступно
  return value.length > 3;
});

Правильный вариант:

Yup.string().test("good", "Ошибка", function (value) {
  return (value || "").length > 3;
});

Использование тестов для нормализации данных

test может использоваться не только для проверки, но и для подготовки данных через побочные механизмы схемы (хотя напрямую трансформация выполняется через transform).

Yup.string().test(
  "trim-check",
  "Некорректное значение",
  function (value) {
    const trimmed = value?.trim();

    if (!trimmed) return this.createError({ message: "Пустое значение" });

    return true;
  }
);

Поведение в составе сложных объектов

При работе с вложенными объектами test получает доступ к структуре на любом уровне:

const schema = Yup.object({
  user: Yup.object({
    profile: Yup.object({
      age: Yup.number().test(
        "adult-check",
        "Возраст должен быть 18+",
        function (value) {
          return value >= 18;
        }
      )
    })
  })
});

this.path в этом случае будет содержать полный путь: user.profile.age, что позволяет точно локализовать ошибки.


Оптимизация тестов при большом количестве правил

При увеличении количества test важно учитывать производительность:

  • избегается повторных API-запросов внутри разных тестов одного поля
  • используется кеширование результатов в замыкании
  • минимизируется использование тяжёлых вычислений

Пример кеширования:

let cache = new Map();

Yup.string().test(
  "cached-check",
  "Ошибка проверки",
  async (value) => {
    if (cache.has(value)) return cache.get(value);

    const result = await fetch(`/api/check?q=${value}`)
      .then((r) => r.json());

    cache.set(value, result.ok);

    return result.ok;
  }
);

Комбинация test и схемных методов Yup

test не заменяет стандартные методы, а дополняет их. Обычно архитектура схемы строится так:

  • базовые проверки (required, min, max)
  • структурные ограничения
  • кастомные бизнес-правила через test
Yup.string()
  .required()
  .min(5)
  .test("custom-rule", "Ошибка бизнес-логики", function (value) {
    return value !== "admin";
  });