Асинхронная валидация в связке с Yup и YupResolver усложняет
тестирование форм, поскольку результат проверки перестаёт быть
детерминированным в момент вызова функции. Любая схема, использующая
.test(), .when() с промисами или внешние
запросы (например, проверка уникальности имени через API), требует
управления временем выполнения и мокирования внешних зависимостей.
Ключевая особенность YupResolver в том, что он преобразует Yup-схему в функцию-резолвер, совместимую с системами управления формами вроде react-hook-form. Эта функция всегда возвращает Promise, даже если внутри схемы нет асинхронных правил. Это означает, что тестовая среда должна учитывать асинхронный характер выполнения на уровне резолвера, а не только схемы.
Любой вызов resolver возвращает структуру:
values — валидированные данные при успехеerrors — объект ошибок при невалидных данныхДаже синхронные проверки оборачиваются в Promise, что приводит к
необходимости использования await в тестах. Типичная ошибка
— попытка проверить результат без ожидания завершения промиса, что
приводит к ложноположительным результатам.
Особенность проявляется особенно ярко при использовании кастомных асинхронных тестов Yup:
const schema = yup.object({
username: yup
.string()
.required()
.test(
'check-username',
'Имя уже занято',
async (value) => {
const res = await fetch(`/api/check?username=${value}`);
const data = await res.json();
return data.available;
}
)
});
В этом случае YupResolver не различает источник асинхронности — он всегда дожидается завершения всех промисов внутри схемы.
Тестирование начинается с изоляции схемы и замены внешних вызовов.
Основной инструмент — мокирование fetch, axios
или любых сервисных функций.
global.fetch = jest.fn();
Далее важно учитывать, что вызов resolver сам по себе асинхронный:
const resolver = yupResolver(schema);
const result = await resolver({
username: 'test'
}, {}, {});
Структура вызова зависит от интеграции, но принцип остаётся: всегда
требуется await.
Сценарий успешной проверки строится на контролируемом ответе API:
test('валидирует уникальный username', async () => {
fetch.mockResolvedValueOnce({
json: async () => ({ available: true })
});
const resolver = yupResolver(schema);
const result = await resolver({
username: 'unique_user'
});
expect(result.errors).toEqual({});
expect(result.values.username).toBe('unique_user');
});
Ключевой момент — стабилизация внешнего состояния. Любая нестабильность моков приводит к флакiness тестов.
При отрицательном сценарии важно убедиться, что ошибка корректно попадает в структуру Yup:
test('возвращает ошибку при занятом username', async () => {
fetch.mockResolvedValueOnce({
json: async () => ({ available: false })
});
const resolver = yupResolver(schema);
const result = await resolver({
username: 'existing_user'
});
expect(result.errors.username).toBeDefined();
expect(result.errors.username.message).toBe('Имя уже занято');
});
YupResolver не выбрасывает исключения — он всегда возвращает нормализованный объект ошибок, что упрощает проверку, но требует внимательной работы с полями результата.
Если схема включает debounce или искусственные задержки через
setTimeout, необходимо подключать управление временем.
jest.useFakeTimers();
Пример схемы с задержкой:
.test('async delay', async (value) => {
await new Promise(res => setTimeout(res, 500));
return value === 'ok';
});
Тестирование:
test('обрабатывает задержку валидации', async () => {
jest.useFakeTimers();
const promise = resolver({ field: 'ok' });
jest.advanceTimersByTime(500);
const result = await promise;
expect(result.errors).toEqual({});
});
Важно учитывать, что не все реализации Yup одинаково стабильно работают с fake timers, особенно при вложенных промисах.
Более устойчивый подход заключается в выносе API-логики из схемы:
const checkUsername = async (name) => {
const res = await api.get(`/check?name=${name}`);
return res.available;
};
Схема:
.test('check-username', async (value) => {
return await checkUsername(value);
});
Тест:
jest.mock('../api');
test('mock service layer', async () => {
checkUsername.mockResolvedValue(true);
const result = await resolver({ username: 'abc' });
expect(result.errors).toEqual({});
});
Такой подход снижает связность тестов с HTTP-слоем и делает Yup-схему более предсказуемой.
При множественных быстрых вызовах возможна ситуация гонки запросов. Это особенно актуально при интеграции YupResolver с UI-формами, где каждая смена поля вызывает новую валидацию.
Тестирование:
test('игнорирует устаревшие ответы', async () => {
let resolveFirst;
let resolveSecond;
fetch
.mockImplementationOnce(() =>
new Promise(res => { resolveFirst = res; })
)
.mockImplementationOnce(() =>
new Promise(res => { resolveSecond = res; })
);
const resolver = yupResolver(schema);
const p1 = resolver({ username: 'a' });
const p2 = resolver({ username: 'ab' });
resolveSecond({
json: async () => ({ available: true })
});
resolveFirst({
json: async () => ({ available: false })
});
const r2 = await p2;
const r1 = await p1;
expect(r2.values?.username).toBe('ab');
});
Такие сценарии выявляют проблемы, которые не видны при одиночных вызовах resolver.
При использовании YupResolver в react-hook-form тестирование часто переносится на уровень пользовательского поведения.
const { result } = renderHook(() =>
useForm({
resolver: yupResolver(schema)
})
);
Далее проверяется submit:
await act(async () => {
await result.current.handleSubmit(() => {} )({
username: 'test'
});
});
Асинхронность здесь проявляется в задержке между submit и обновлением состояния ошибок.
Если схема содержит несколько .test() с промисами, Yup
выполняет их последовательно или параллельно в зависимости от версии.
Это влияет на стабильность тестов.
yup.object({
field: yup
.string()
.test('t1', async () => true)
.test('t2', async () => true)
})
При тестировании важно учитывать, что порядок выполнения не гарантирует порядок завершения, поэтому проверки должны быть независимыми от времени исполнения.
Наиболее распространённые проблемы:
await при вызове resolverКаждая из этих ошибок приводит к нестабильным тестам, которые проявляются только при нагрузочном запуске или параллельном исполнении.
Асинхронная валидация в YupResolver требует выстраивания тестовой архитектуры вокруг Promise-модели выполнения. Основная сложность заключается не в самой Yup-схеме, а в синхронизации всех уровней: резолвера, моков, таймеров и UI-интеграции.