Кастомные валидаторы в Yup часто становятся критическим элементом бизнес-логики, особенно когда стандартных правил недостаточно для описания доменной модели. Именно такие расширения требуют отдельного подхода к тестированию, поскольку их корректность напрямую влияет на валидацию форм, API-входов и внутренних структур данных.
В Yup кастомная логика реализуется через test,
addMethod или композицию схем. Несмотря на различия в
реализации, все такие валидаторы обладают общим свойством — они
инкапсулируют произвольные правила, которые невозможно выразить
стандартными методами (required, min,
max, email и т.д.).
Типичный пример кастомного валидатора:
import * as Yup from 'yup';
const schema = Yup.string().test(
'no-spaces',
'Строка не должна содержать пробелы',
(value) => !value || !value.includes(' ')
);
Подобная логика требует проверки не только «положительных» сценариев,
но и всех пограничных состояний, включая null,
undefined, пустые строки и неожиданные типы.
Тестирование Yup-схем строится вокруг метода validate
или isValid. На практике чаще используется
validate, так как он позволяет получить детализированную
ошибку.
Пример теста с использованием Jest:
import * as Yup from 'yup';
const schema = Yup.string().test(
'no-spaces',
'no spaces allowed',
(value) => !value || !value.includes(' ')
);
test('rejects strings with spaces', async () => {
await expect(schema.validate('hello world')).rejects.toThrow('no spaces allowed');
});
test('accepts strings without spaces', async () => {
await expect(schema.validate('helloworld')).resolves.toBe('helloworld');
});
Ключевой момент заключается в том, что Yup возвращает промисы, поэтому тесты должны учитывать асинхронную природу валидации.
Кастомные валидаторы почти всегда содержат скрытые условия, которые проявляются только на edge cases. Основные категории таких значений:
undefinednull''' 'Пример расширенного тестирования:
test('handles undefined', async () => {
await expect(schema.validate(undefined)).resolves.toBeUndefined();
});
test('handles null', async () => {
await expect(schema.validate(null)).resolves.toBeNull();
});
test('rejects whitespace-only string', async () => {
await expect(schema.validate(' ')).rejects.toThrow();
});
Подобный набор проверок предотвращает ситуации, когда валидатор работает корректно только в рамках «идеальных» входных данных.
На практике кастомные валидаторы чаще всего используются внутри
Yup.object(). В этом случае важно тестировать не отдельное
поле, а поведение всей схемы.
const schema = Yup.object({
username: Yup.string().test(
'no-admin',
'invalid username',
(value) => value !== 'admin'
)
});
Тестирование:
test('rejects forbidden username', async () => {
await expect(schema.validate({ username: 'admin' }))
.rejects.toThrow('invalid username');
});
test('accepts valid username', async () => {
await expect(schema.validate({ username: 'user1' }))
.resolves.toMatchObject({ username: 'user1' });
});
Особое внимание следует уделять частичным объектам, когда некоторые поля отсутствуют.
Yup поддерживает асинхронные проверки, например запросы к API или базе данных:
const schema = Yup.string().test(
'is-unique',
'already exists',
async (value) => {
const exists = await fakeApiCheck(value);
return !exists;
}
);
Тестирование таких валидаторов требует мокирования внешних зависимостей:
jest.mock('./api', () => ({
fakeApiCheck: jest.fn()
}));
import { fakeApiCheck } from './api';
test('rejects existing value', async () => {
fakeApiCheck.mockResolvedValue(true);
await expect(schema.validate('test')).rejects.toThrow('already exists');
});
test('accepts unique value', async () => {
fakeApiCheck.mockResolvedValue(false);
await expect(schema.validate('test')).resolves.toBe('test');
});
Ключевой аспект — контроль детерминированности. Асинхронная логика должна быть полностью управляемой в тестах.
Когда кастомный валидатор содержит множество условий, удобно применять table-driven подход.
const cases = [
['admin', false],
['root', false],
['user', true],
['guest', true]
];
test.each(cases)('validates username %s', async (input, expected) => {
if (expected) {
await expect(schema.validate(input)).resolves.toBe(input);
} else {
await expect(schema.validate(input)).rejects.toThrow();
}
});
Такой подход уменьшает дублирование и повышает читаемость тестов.
Yup позволяет использовать transform, который изменяет
входные данные до валидации. Это создаёт дополнительный слой, который
обязательно должен быть протестирован.
const schema = Yup.number().transform((value, originalValue) => {
return typeof originalValue === 'string'
? parseInt(originalValue, 10)
: value;
});
Тест:
test('transforms string to number', async () => {
await expect(schema.validate('10')).resolves.toBe(10);
});
Игнорирование трансформаций часто приводит к ошибкам, когда валидатор тестируется только на уже нормализованных данных.
Yup возвращает промисы, и отсутствие await приводит к
ложноположительным тестам.
Валидация без негативных кейсов не отражает реального поведения системы.
Любая внешняя зависимость должна быть изолирована, иначе тесты становятся нестабильными.
Иногда тестируется только факт ошибки, без проверки конкретного сообщения или причины, что снижает точность диагностики.
При росте количества кастомных валидаторов становится важна организация тестов:
Пример структуры:
validation/
schemas/
user.schema.js
__tests__/
user.schema.test.js
validators/
noSpaces.test.js
uniqueUsername.test.js
Такой подход снижает связанность и упрощает сопровождение.
В Yup часто комбинируются несколько кастомных правил:
Yup.string()
.test('no-spaces', ...)
.test('no-special-chars', ...)
.test('not-admin', ...)
Тестирование должно учитывать порядок выполнения и потенциальные конфликты:
test('fails on first invalid rule', async () => {
await expect(schema.validate('admin user')).rejects.toThrow();
});
Важно фиксировать не только факт ошибки, но и предсказуемость поведения при цепочке проверок.
Наиболее устойчивые подходы к тестированию кастомных валидаторов включают:
Особую роль играет повторяемость: тесты должны оставаться детерминированными независимо от среды выполнения.
Кастомные валидаторы в Yup становятся надёжным элементом системы только тогда, когда их поведение полностью описано тестами, охватывающими как типичные, так и нетривиальные сценарии использования.