Во многих формах часть правил валидации зависит от структуры данных. Простая проверка отдельных полей быстро становится неудобной, если форма содержит:
В таких случаях Vest предоставляет механизм вложенных тестов, позволяющий организовать проверки в виде логических блоков.
Основная цель вложенных тестов — сгруппировать связанные проверки и построить иерархическую структуру валидации.
Без вложенности тесты быстро превращаются в длинный линейный список:
test('street', 'Укажите улицу', () => {
enforce(data.street).isNotBlank();
});
test('city', 'Укажите город', () => {
enforce(data.city).isNotBlank();
});
test('zip', 'Укажите индекс', () => {
enforce(data.zip).matches(/^\d{6}$/);
});
test('cardNumber', 'Номер карты неверен', () => {
enforce(data.cardNumber).matches(/^\d{16}$/);
});
test('cvv', 'CVV неверен', () => {
enforce(data.cvv).matches(/^\d{3}$/);
});
При увеличении количества секций возникают проблемы:
Вложенные тесты решают эти ограничения.
groupVest позволяет объединять тесты в логические группы.
import { create, test, enforce, group } from 'vest';
const suite = create((data) => {
group('address', () => {
test('street', 'Укажите улицу', () => {
enforce(data.street).isNotBlank();
});
test('city', 'Укажите город', () => {
enforce(data.city).isNotBlank();
});
test('zip', 'Неверный индекс', () => {
enforce(data.zip).matches(/^\d{6}$/);
});
});
group('payment', () => {
test('cardNumber', 'Неверный номер карты', () => {
enforce(data.cardNumber).matches(/^\d{16}$/);
});
test('cvv', 'Неверный CVV', () => {
enforce(data.cvv).matches(/^\d{3}$/);
});
});
});
groupФункция group:
Группы могут быть вложенными друг в друга.
const suite = create((data) => {
group('checkout', () => {
group('address', () => {
test('street', 'Укажите улицу', () => {
enforce(data.address.street).isNotBlank();
});
test('city', 'Укажите город', () => {
enforce(data.address.city).isNotBlank();
});
});
group('payment', () => {
group('card', () => {
test('number', 'Неверный номер карты', () => {
enforce(data.payment.card.number).matches(/^\d{16}$/);
});
test('cvv', 'Неверный CVV', () => {
enforce(data.payment.card.cvv).matches(/^\d{3}$/);
});
});
});
});
});
Одна из важнейших задач вложенных тестов — проверка массивов объектов.
Например:
const users = [
{
name: 'Alex',
email: 'alex@mail.com'
},
{
name: '',
email: 'wrong-email'
}
];
const suite = create((data) => {
data.users.forEach((user, index) => {
group(`user_${index}`, () => {
test(`name_${index}`, 'Имя обязательно', () => {
enforce(user.name).isNotBlank();
});
test(`email_${index}`, 'Email некорректен', () => {
enforce(user.email).isEmail();
});
});
});
});
Без группировки:
Группы позволяют строить независимые области проверки для каждого элемента массива.
Многие формы содержат динамические блоки:
Vest позволяет валидировать их динамически.
const suite = create((data) => {
data.items.forEach((item, index) => {
group(`item_${index}`, () => {
test(`title_${index}`, 'Название обязательно', () => {
enforce(item.title).isNotBlank();
});
test(`price_${index}`, 'Цена должна быть больше нуля', () => {
enforce(item.price).greaterThan(0);
});
test(`quantity_${index}`, 'Количество должно быть больше нуля', () => {
enforce(item.quantity).greaterThan(0);
});
});
});
});
Крупные формы часто требуют переиспользования одинаковых наборов правил.
Vest позволяет выносить группы в отдельные функции.
function validateAddress(address) {
group('address', () => {
test('street', 'Улица обязательна', () => {
enforce(address.street).isNotBlank();
});
test('city', 'Город обязателен', () => {
enforce(address.city).isNotBlank();
});
test('zip', 'Индекс неверен', () => {
enforce(address.zip).matches(/^\d{6}$/);
});
});
}
const suite = create((data) => {
validateAddress(data.billingAddress);
validateAddress(data.shippingAddress);
});
Вложенные тесты позволяют разделять ответственность между модулями.
const suite = create((data) => {
// сотни тестов подряд
});
const suite = create((data) => {
validateProfile(data.profile);
validateContacts(data.contacts);
validateDocuments(data.documents);
validatePayment(data.payment);
});
Каждая функция содержит собственные вложенные группы.
Вложенные проверки особенно полезны при условной логике.
const suite = create((data) => {
test('type', 'Выберите тип аккаунта', () => {
enforce(data.type).isNotBlank();
});
if (data.type === 'company') {
group('company', () => {
test('companyName', 'Название компании обязательно', () => {
enforce(data.companyName).isNotBlank();
});
test('taxId', 'ИНН обязателен', () => {
enforce(data.taxId).isNotBlank();
});
});
}
});
Vest поддерживает асинхронные проверки внутри групп.
const suite = create(async (data) => {
group('users', () => {
data.users.forEach((user, index) => {
test(`email_${index}`, 'Email уже существует', async () => {
const exists = await checkEmail(user.email);
enforce(exists).isFalsy();
});
});
});
});
При сложной вложенности полезно использовать пути объектов.
test('profile.address.city', 'Город обязателен', () => {
enforce(data.profile.address.city).isNotBlank();
});
Иерархические имена:
Vest хорошо подходит для композиции.
function validatePhone(phone, prefix) {
group(prefix, () => {
test(`${prefix}.countryCode`, 'Код страны обязателен', () => {
enforce(phone.countryCode).isNotBlank();
});
test(`${prefix}.number`, 'Номер обязателен', () => {
enforce(phone.number).isNotBlank();
});
});
}
const suite = create((data) => {
validatePhone(data.homePhone, 'homePhone');
validatePhone(data.workPhone, 'workPhone');
});
Плохой пример:
group('user', () => {
test('email', 'Ошибка', () => {});
});
group('admin', () => {
test('email', 'Ошибка', () => {});
});
В больших системах одинаковые имена могут затруднить диагностику.
group('user', () => {
test('user.email', 'Ошибка', () => {});
});
group('admin', () => {
test('admin.email', 'Ошибка', () => {});
});
Слишком глубокая структура усложняет поддержку.
group('a', () => {
group('b', () => {
group('c', () => {
group('d', () => {
group('e', () => {
// ...
});
});
});
});
});
Оптимальная вложенность:
Структурированные проверки особенно полезны при интеграции с интерфейсами.
{
profile: {
email: ['Email неверен']
},
address: {
city: ['Город обязателен']
}
}
Такая структура:
В крупных проектах вложенные тесты становятся обязательным архитектурным инструментом.
const suite = create((data) => {
validateAccount(data.account);
validateProfile(data.profile);
validateSecurity(data.security);
validateNotifications(data.notifications);
validateBilling(data.billing);
});
function validateSecurity(security) {
group('security', () => {
test('security.password', 'Пароль слишком короткий', () => {
enforce(security.password).longerThanOrEquals(8);
});
test('security.confirmPassword', 'Пароли не совпадают', () => {
enforce(security.confirmPassword).equals(security.password);
});
});
}
eachVest поддерживает вспомогательные механизмы для работы с коллекциями.
import { each } from 'vest';
const suite = create((data) => {
each(data.users, (user, index) => {
group(`user_${index}`, () => {
test(`email_${index}`, 'Email неверен', () => {
enforce(user.email).isEmail();
});
});
});
});
При работе с вложенными тестами полезно разделять валидаторы по доменам.
validation/
├── account/
│ ├── accountSuite.js
│ ├── profileValidation.js
│ ├── securityValidation.js
│ └── addressValidation.js
│
├── order/
│ ├── orderSuite.js
│ ├── itemsValidation.js
│ └── paymentValidation.js
Правильно:
group('billing', () => {});
group('shipping', () => {});
Неправильно:
group('group1', () => {});
group('group2', () => {});
Хорошая структура:
profile.email
profile.phone
profile.address.city
Повторяющиеся группы необходимо выносить в функции.
Это облегчает:
import { create, test, enforce, group } from 'vest';
function validateAddress(address, prefix) {
group(prefix, () => {
test(`${prefix}.street`, 'Улица обязательна', () => {
enforce(address.street).isNotBlank();
});
test(`${prefix}.city`, 'Город обязателен', () => {
enforce(address.city).isNotBlank();
});
test(`${prefix}.zip`, 'Индекс неверен', () => {
enforce(address.zip).matches(/^\d{6}$/);
});
});
}
function validateUser(user, index) {
group(`user_${index}`, () => {
test(`user_${index}.name`, 'Имя обязательно', () => {
enforce(user.name).isNotBlank();
});
test(`user_${index}.email`, 'Email неверен', () => {
enforce(user.email).isEmail();
});
validateAddress(user.address, `user_${index}.address`);
});
}
const suite = create((data) => {
data.users.forEach((user, index) => {
validateUser(user, index);
});
});
Такой подход обеспечивает: