Асинхронные правила валидации в схемах Vest применяются там, где проверка зависит от внешних источников данных: сетевых запросов, обращений к API, базы данных или любых операций с задержкой. В отличие от синхронных проверок, такие правила возвращают результат не сразу, а через Promise, что требует отдельного подхода как при реализации, так и при тестировании.
Ключевая особенность асинхронных правил заключается в том, что состояние результата формируется постепенно. Схема может быть запущена, но финальная валидность поля становится известна только после завершения всех асинхронных операций.
Асинхронное правило в Vest представляет собой функцию, возвращающую Promise. Внутри таких правил обычно выполняются запросы к внешним сервисам или имитации этих запросов в тестовой среде.
Типовая структура:
import { test, enforce } from 'vest';
test('email', 'EMAIL_ALREADY_USED', async () => {
const res = await fetch(`/api/check-email?value=${value}`);
const data = await res.json();
enforce(data.available).isTruthy();
});
Особенность выполнения заключается в том, что Vest не блокирует выполнение всей схемы, а параллелит асинхронные проверки, сохраняя общий поток выполнения валидации.
Тестирование асинхронных правил требует учета нескольких факторов:
Основная ошибка при тестировании — проверка состояния до завершения всех асинхронных операций.
Наиболее прямой способ — ожидание результата выполнения схемы.
import { suite } from 'vest';
const userSuite = (data) =>
suite('user', () => {
test('username', 'USERNAME_TAKEN', async () => {
const res = await fetch(`/api/username?u=${data.username}`);
const json = await res.json();
enforce(json.available).isTruthy();
});
});
Тест:
it('проверяет доступность username', async () => {
fetch.mockResponseOnce(JSON.stringify({ available: false }));
const result = await userSuite({ username: 'john' });
expect(result.hasErrors('username')).toBe(true);
});
Ключевой момент — тест сам должен быть асинхронным и дожидаться результата выполнения suite.
Асинхронные правила почти всегда зависят от сети, поэтому тестирование требует мокирования.
global.fetch = jest.fn();
Или более точечно:
fetch.mockImplementation(() =>
Promise.resolve({
json: () => Promise.resolve({ available: true }),
})
);
Такая изоляция позволяет полностью контролировать поведение асинхронного правила без обращения к реальному API.
Асинхронные правила должны корректно подтверждать валидные данные.
it('не возвращает ошибку, если username доступен', async () => {
fetch.mockResolvedValue({
json: () => Promise.resolve({ available: true }),
});
const result = await userSuite({ username: 'valid_user' });
expect(result.hasErrors('username')).toBe(false);
});
Здесь важен контроль состояния после полного завершения suite, иначе результат может быть частичным.
При отрицательном ответе API схема должна фиксировать ошибку.
it('возвращает ошибку, если username занят', async () => {
fetch.mockResolvedValue({
json: () => Promise.resolve({ available: false }),
});
const result = await userSuite({ username: 'taken_user' });
expect(result.hasErrors('username')).toBe(true);
});
В подобных случаях проверяется не только наличие ошибки, но и корректная привязка к конкретному полю.
Асинхронные правила могут запускаться параллельно. Это создаёт риск гонок состояний при тестировании.
Типичная ситуация:
Для тестирования таких сценариев используется контроль порядка резолва Promise:
fetch
.mockImplementationOnce(() =>
new Promise((resolve) =>
setTimeout(
() => resolve({ json: () => Promise.resolve({ available: true }) }),
50
)
)
)
.mockImplementationOnce(() =>
Promise.resolve({
json: () => Promise.resolve({ available: false }),
})
);
Проверка:
it('обрабатывает конкурирующие запросы корректно', async () => {
const result = await userSuite({ username: 'race_user' });
expect(result.hasErrors('username')).toBe(true);
});
При тестировании задержек удобно использовать fake timers, особенно если внутри правил присутствует debounce или искусственные задержки.
jest.useFakeTimers();
Асинхронная логика:
test('username', async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
enforce(false).equals(true);
});
Тест:
it('корректно отрабатывает задержку', async () => {
const promise = userSuite({ username: 'delayed' });
jest.advanceTimersByTime(300);
const result = await promise;
expect(result.hasErrors('username')).toBe(true);
});
Vest допускает смешанные схемы, где синхронные и асинхронные правила работают одновременно. Это усложняет тестирование, поскольку порядок завершения становится непредсказуемым.
test('username', () => {
enforce(value).isNotEmpty();
return fetch(`/api/check?u=${value}`)
.then((r) => r.json())
.then((data) => {
enforce(data.available).isTruthy();
});
});
В таких случаях тестирование всегда строится вокруг финального результата suite, а не отдельных правил.
Иногда требуется протестировать состояние до завершения всех async операций. Это возможно только через контроль выполнения Promise и частичное ожидание.
const promise = userSuite({ username: 'pending' });
const partial = userSuite.get(); // промежуточное состояние
expect(partial.isValid('username')).toBeUndefined();
const result = await promise;
expect(result.hasErrors('username')).toBe(true);
Промежуточное состояние используется только для проверки поведения UI-логики между вызовами.
При сложной архитектуре fetch обычно выносится в отдельный слой:
import api from '../api/user';
Тестирование:
jest.mock('../api/user', () => ({
checkUsername: jest.fn(),
}));
Использование в схеме:
test('username', async () => {
const res = await api.checkUsername(value);
enforce(res.available).isTruthy();
});
Такой подход упрощает тестирование, устраняя зависимость от глобального fetch.
Распространённые проблемы:
await при вызове suiteКаждая из этих ошибок приводит к нестабильным или флаки-тестам, особенно при параллельном запуске тестового набора.
Если асинхронное правило выбрасывает исключение, Vest фиксирует его как ошибку поля.
test('email', async () => {
throw new Error('network failure');
});
Тестирование:
it('обрабатывает ошибки сети', async () => {
fetch.mockRejectedValue(new Error('network'));
const result = await userSuite({ email: 'a@b.com' });
expect(result.hasErrors('email')).toBe(true);
});
Когда несколько асинхронных правил работают одновременно, тест должен учитывать агрегированное состояние.
test('email', async () => { /* check 1 */ });
test('email', async () => { /* check 2 */ });
test('email', async () => { /* check 3 */ });
Результат объединяется, и тестирование всегда ориентируется на финальный агрегированный state:
const result = await userSuite({ email: 'test@mail.com' });
expect(result.hasErrors('email')).toBe(true);