В библиотеке Vest тест представляет собой не отдельную функцию проверки, а часть декларативного сценария валидации. Структура теста определяет:
Базовая единица структуры — вызов test().
import { create, test, enforce } from 'vest';
const suite = create((data = {}) => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
});
Здесь:
| Элемент | Назначение |
|---|---|
test |
регистрация теста |
'email' |
имя валидируемого поля |
'Некорректный email' |
сообщение ошибки |
| callback | логика проверки |
Полная структура:
test(fieldName, message, callback);
Имя поля связывает тест с конкретным свойством объекта.
test('password', 'Пароль слишком короткий', () => {
enforce(data.password).longerThanOrEquals(8);
});
Vest использует имя поля для:
Сообщение ошибки может быть:
Пример со строкой:
test('username', 'Имя пользователя занято', () => {
enforce(data.username).notEquals('admin');
});
Пример с функцией:
test('age', () => `Возраст должен быть больше 18`, () => {
enforce(data.age).greaterThan(18);
});
Функция проверки содержит основной код валидации.
test('email', 'Email обязателен', () => {
enforce(data.email).isNotBlank();
});
Если внутри callback возникает ошибка, Vest помечает тест как failed.
Каждый тест проходит несколько этапов:
Схематично:
test()
↓
Регистрация поля
↓
Выполнение callback
↓
Ошибка?
├── Да → failed
└── Нет → passed
↓
Обновление состояния
Наиболее распространённая структура.
test('firstName', 'Введите имя', () => {
enforce(data.firstName).isNotBlank();
});
Проверка завершается немедленно.
Vest поддерживает Promise и async/await.
test(
'email',
'Email уже используется',
async () => {
const exists = await api.checkEmail(data.email);
enforce(exists).isFalsy();
}
);
Во время выполнения тест получает состояние:
pending
После завершения:
passed / failed
test('username', 'Ошибка проверки', async () => {
const response = await fetch('/check');
if (!response.ok) {
throw new Error('Network error');
}
});
Любое исключение внутри async callback интерпретируется как ошибка теста.
Vest строит дерево зависимостей между тестами.
test('email', 'Неверный email', () => {
enforce(data.email).isEmail();
});
test('password', 'Слабый пароль', () => {
enforce(data.password).longerThan(8);
});
Оба теста выполняются независимо.
Несколько тестов могут относиться к одному полю.
test('password', 'Пароль обязателен', () => {
enforce(data.password).isNotBlank();
});
test('password', 'Минимум 8 символов', () => {
enforce(data.password).longerThanOrEquals(8);
});
test('password', 'Нужна цифра', () => {
enforce(data.password).matches(/[0-9]/);
});
Vest сохраняет все ошибки отдельно.
import { group } from 'vest';
group('authentication', () => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
test('password', 'Пароль обязателен', () => {
enforce(data.password).isNotBlank();
});
});
Группы используются для:
Тест может зависеть от другого поля.
import { when } from 'vest';
when(data.country === 'US', () => {
test('zip', 'Некорректный ZIP-код', () => {
enforce(data.zip).matches(/^\d{5}$/);
});
});
Тест создаётся только при выполнении условия.
Пропуск теста.
import { skip } from 'vest';
skip(!data.email, () => {
test('email', 'Неверный email', () => {
enforce(data.email).isEmail();
});
});
Если условие истинно — тест не запускается.
Полное исключение блока тестов.
import { omitWhen } from 'vest';
omitWhen(data.isGuest, () => {
test('password', 'Введите пароль', () => {
enforce(data.password).isNotBlank();
});
});
Разница между skip и omitWhen:
| Метод | Поведение |
|---|---|
skip |
тест существует, но не выполняется |
omitWhen |
тест полностью исключается |
Vest умеет прекращать выполнение после первой ошибки.
import { only } from 'vest';
only('email');
Будут выполняться только тесты поля email.
import { skipWhen } from 'vest';
skipWhen(res => res.hasErrors('email'), () => {
test('password', 'Пароль слишком короткий', () => {
enforce(data.password).shorterThan(8);
});
});
Структура зависимой валидации:
email failed
↓
password skipped
function validatePassword(password) {
enforce(password).longerThanOrEquals(8);
enforce(password).matches(/[A-Z]/);
}
test('password', 'Слабый пароль', () => {
validatePassword(data.password);
});
function required(field, value) {
test(field, `${field} обязателен`, () => {
enforce(value).isNotBlank();
});
}
required('email', data.email);
required('username', data.username);
Vest допускает композицию внутри callback.
group('profile', () => {
test('firstName', 'Введите имя', () => {
enforce(data.firstName).isNotBlank();
});
group('contacts', () => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
});
});
Это особенно полезно в больших формах.
Тесты могут создаваться во время выполнения.
const fields = ['email', 'username', 'password'];
fields.forEach(field => {
test(field, `${field} обязателен`, () => {
enforce(data[field]).isNotBlank();
});
});
const rules = {
username: value => enforce(value).longerThan(3),
password: value => enforce(value).longerThan(8),
};
Object.entries(rules).forEach(([field, validator]) => {
test(field, 'Ошибка валидации', () => {
validator(data[field]);
});
});
Каждый тест хранит:
{
fieldName,
message,
status,
async,
pending
}
Пример:
{
fieldName: 'email',
message: 'Некорректный email',
status: 'failed',
async: false,
pending: false
}
Vest использует исключения как механизм определения failed state.
test('age', 'Возраст должен быть больше 18', () => {
enforce(data.age).greaterThan(18);
});
Если исключение не возникло:
status = passed
test('age', 'Возраст должен быть больше 18', () => {
throw new Error();
});
Результат:
status = failed
После выполнения suite:
const result = suite(data);
Доступны методы:
result.hasErrors();
result.getErrors();
result.isValid();
Структура тестов напрямую влияет на содержимое state.
Vest умеет запускать только часть тестов.
suite(data, 'email');
Будут выполнены только тесты поля email.
Это особенно важно для:
Проверка может выполняться во время ввода.
input.addEventListener('input', e => {
suite({ email: e.target.value }, 'email');
});
Структура тестов должна быть:
Нежелательная структура:
test('email', 'Ошибка', () => {
saveUser(data);
});
Причины:
Корректный подход:
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
test('billingAddress', 'Введите адрес', () => {});
test('confirmPassword', 'Пароли не совпадают', () => {});
test('field1', 'Ошибка', () => {});
test('x', 'Invalid', () => {});
Имя поля должно:
// userSuite.js
export function validateUser(data) {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
}
// mainSuite.js
create(data => {
validateUser(data);
});
Типичная структура:
validation/
├── user/
│ ├── email.js
│ ├── password.js
│ └── profile.js
├── billing/
├── shipping/
└── shared/
Vest выполняет тесты сверху вниз.
test('a', 'A', () => {});
test('b', 'B', () => {});
test('c', 'C', () => {});
Порядок:
a → b → c
Однако async тесты завершаются независимо.
test('email', 'Ошибка email', async () => {});
test('username', 'Ошибка username', async () => {});
Внутренне:
register
↓
start async jobs
↓
pending state
↓
resolve independently
Vest кэширует результаты тестов.
Повторный запуск:
suite(data);
suite(data);
может не вызывать повторную проверку, если входные данные не изменились.
Это снижает:
import {
create,
test,
enforce,
group,
skipWhen,
} from 'vest';
export const suite = create(data => {
group('auth', () => {
test('email', 'Введите email', () => {
enforce(data.email).isNotBlank();
});
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
skipWhen(
res => res.hasErrors('email'),
() => {
test(
'email',
'Email уже используется',
async () => {
const exists =
await api.emailExists(data.email);
enforce(exists).isFalsy();
}
);
}
);
test('password', 'Минимум 8 символов', () => {
enforce(data.password)
.longerThanOrEquals(8);
});
test(
'confirmPassword',
'Пароли не совпадают',
() => {
enforce(data.confirmPassword)
.equals(data.password);
}
);
});
});