Схемы в Yup представляют собой декларативное описание правил валидации данных, и их тестирование строится вокруг проверки предсказуемости этих правил. Ключевая цель unit-тестов — гарантировать, что каждая ветка валидации ведёт себя строго детерминированно при одинаковых входных данных.
В контексте схем Yup тестирование обычно делится на несколько уровней:
Основной принцип: схема должна быть протестирована как чистая функция, без зависимости от UI, формы или состояния приложения.
Для тестирования Yup-схем чаще всего используются Jest или Vitest. Обе библиотеки подходят одинаково хорошо, так как Yup возвращает Promise при валидации.
Базовая настройка включает установку тестового раннера и саму библиотеку Yup:
npm install yup
npm install -D jest
При использовании TypeScript дополнительно подключается типизация:
npm install -D @types/jest ts-jest
В Vitest конфигурация обычно проще:
npm install -D vitest
Любая Yup-схема тестируется через методы:
validateisValidvalidateSyncНа практике чаще используется validate, так как он
возвращает детализированную ошибку.
Пример базовой схемы:
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email().required(),
});
Тестирование валидного случая:
test("валидный email проходит проверку", async () => {
const data = { email: "test@mail.com" };
await expect(schema.validate(data)).resolves.toEqual(data);
});
Тестирование ошибки:
test("пустой email вызывает ошибку", async () => {
const data = { email: "" };
await expect(schema.validate(data)).rejects.toBeTruthy();
});
Одна из самых частых проверок — required. В Yup она
работает в сочетании с типом данных.
const schema = yup.object({
username: yup.string().required(),
});
Тесты должны покрывать:
undefinednulltest("username обязателен", async () => {
await expect(schema.validate({ username: undefined })).rejects.toBeTruthy();
await expect(schema.validate({ username: null })).rejects.toBeTruthy();
await expect(schema.validate({ username: "" })).rejects.toBeTruthy();
});
Строковые правила часто включают:
minmaxmatchesemailurlПример схемы:
const schema = yup.object({
password: yup.string().min(8).max(20),
});
Тестирование граничных значений:
test("password соблюдает длину", async () => {
await expect(schema.validate({ password: "1234567" })).rejects.toBeTruthy();
await expect(schema.validate({ password: "12345678" })).resolves.toBeTruthy();
await expect(schema.validate({ password: "a".repeat(20) })).resolves.toBeTruthy();
await expect(schema.validate({ password: "a".repeat(21) })).rejects.toBeTruthy();
});
Числа требуют отдельного внимания, особенно при работе с формами, где значения часто приходят как строки.
const schema = yup.object({
age: yup.number().min(18).max(60),
});
Типичная ошибка — передача строки:
test("строка не проходит числовую проверку", async () => {
await expect(schema.validate({ age: "20" })).rejects.toBeTruthy();
});
Если используется преобразование:
age: yup.number().transform((value, originalValue) => Number(originalValue))
Тогда тест должен учитывать корректную трансформацию:
test("строка преобразуется в число", async () => {
await expect(schema.validate({ age: "20" })).resolves.toEqual({ age: 20 });
});
Метод test() позволяет задавать пользовательскую
логику.
const schema = yup.object({
username: yup.string().test(
"no-admin",
"username не может быть admin",
(value) => value !== "admin"
),
});
Тестирование кастомной логики:
test("admin запрещён", async () => {
await expect(schema.validate({ username: "admin" })).rejects.toBeTruthy();
});
test("другие значения разрешены", async () => {
await expect(schema.validate({ username: "user" })).resolves.toBeTruthy();
});
Важно проверять не только true/false, но и корректность
сообщения ошибки через ValidationError.
Асинхронные проверки часто используются для проверки уникальности значений через API.
const schema = yup.object({
email: yup.string().test(
"unique-email",
"email уже используется",
async (value) => {
const exists = await fakeApiCheck(value);
return !exists;
}
),
});
Тестирование асинхронной логики требует моков:
jest.mock("./api", () => ({
fakeApiCheck: jest.fn(),
}));
Пример теста:
test("email должен быть уникальным", async () => {
fakeApiCheck.mockResolvedValue(true);
await expect(schema.validate({ email: "test@mail.com" }))
.rejects
.toBeTruthy();
});
Yup позволяет модифицировать входные данные до валидации. Это часто источник скрытых багов.
const schema = yup.object({
name: yup.string().transform((value) => value.trim()),
});
Тест должен проверять именно результат трансформации:
test("trim удаляет пробелы", async () => {
const result = await schema.validate({ name: " John " });
expect(result.name).toBe("John");
});
Сложные формы часто содержат вложенные структуры:
const schema = yup.object({
user: yup.object({
email: yup.string().email().required(),
profile: yup.object({
age: yup.number().min(18),
}),
}),
});
Тестирование должно покрывать путь к каждому полю:
test("вложенная валидация работает", async () => {
const data = {
user: {
email: "test@mail.com",
profile: { age: 20 },
},
};
await expect(schema.validate(data)).resolves.toEqual(data);
});
Массивы часто используются для динамических форм.
const schema = yup.object({
tags: yup.array().of(yup.string().min(2)),
});
Тестирование:
test("массив валидируется по каждому элементу", async () => {
await expect(schema.validate({ tags: ["ok", "a"] }))
.rejects
.toBeTruthy();
await expect(schema.validate({ tags: ["good", "valid"] }))
.resolves
.toBeTruthy();
});
При росте схемы важно поддерживать читаемую структуру тестов.
Практика группировки через describe позволяет отражать
структуру объекта.
describe("user schema", () => {
describe("email", () => {
test("обязателен", () => {});
test("валидный формат", () => {});
});
describe("password", () => {
test("минимальная длина", () => {});
});
});
Такая организация снижает когнитивную нагрузку при поддержке больших схем.
Иногда важно не только факт ошибки, но и её текст.
try {
await schema.validate({ email: "invalid" });
} catch (err) {
expect(err.message).toContain("email must be a valid email");
}
Это особенно важно в проектах, где сообщения ошибок отображаются пользователю напрямую.
Наиболее частые проблемные зоны:
Тесты должны сознательно включать такие входные данные:
await schema.validate({ age: NaN });
await schema.validate({ email: " " });
await schema.validate({ tags: [null, "ok"] });
Для больших проектов применяется подход выделения фабрик данных:
const validUser = (overrides = {}) => ({
email: "test@mail.com",
password: "12345678",
...overrides,
});
Это позволяет писать тесты без дублирования структуры:
test("email обязателен", async () => {
await expect(schema.validate(validUser({ email: "" })))
.rejects
.toBeTruthy();
});
При использовании TypeScript схема часто становится источником типов.
type User = yup.InferType<typeof schema>;
Тесты могут дополнительно проверять соответствие структуры:
const user: User = await schema.validate(data);
Ошибки типизации на этом этапе часто выявляют несоответствия между схемой и бизнес-логикой.
Схемы Yup должны быть полностью детерминированными. Любая зависимость от внешнего состояния (например, глобальных переменных или кэша API) делает тесты нестабильными.
Поэтому: