Промисы в тестах

Асинхронные проверки валидации в Vest строятся вокруг промисов как базового механизма представления отложенного результата. Любая операция, зависящая от внешнего источника данных — сетевой запрос, обращение к базе, проверка уникальности значения — неизбежно выходит за пределы синхронного исполнения, и тестовая модель должна учитывать три состояния: ожидание, успешное завершение и отклонение.

Внутри тестовой системы промис становится не просто способом получения результата, а частью контрактной модели: тест считается завершённым только после разрешения всех асинхронных зависимостей.

Промис как элемент тестового контракта

Асинхронный тест валидации фактически описывает правило:

  • входные данные инициируют проверку
  • проверка возвращает промис
  • промис завершает тест

Если результат промиса не обработан, тестовая система не может зафиксировать итог состояния, что приводит либо к зависанию, либо к ложноположительным результатам.

Базовая форма асинхронного теста:

const suite = createSuite("user validation", (data) => {
  test("email", "email already exists", async () => {
    return checkEmailExists(data.email);
  });
});

Функция checkEmailExists возвращает промис, который либо резолвится (валидное состояние), либо реджектится (ошибка валидации).

Разрешение промисов в тестовом контексте

Внутри системы валидации промис интерпретируется не как просто результат, а как источник состояния поля. Поведение определяется следующим образом:

  • resolved → тест пройден
  • rejected → тест провален
  • pending → состояние ожидания

Особенность подхода заключается в том, что состояние pending должно быть управляемым. Это означает, что система должна агрегировать все активные промисы перед финальной фиксацией результата.

Пример обработки нескольких асинхронных правил:

test("username", "must be unique", () => {
  return Promise.all([
    checkUsernameAvailability(value),
    checkBlacklist(value),
  ]);
});

Здесь важна деталь: отклонение любого промиса приводит к провалу теста, поэтому композиция промисов становится частью логики валидации.

Интеграция async/await в тестовую модель

Использование async/await упрощает чтение тестов, но не изменяет их семантику. Внутри тестового раннера async функция преобразуется в промис автоматически.

test("email format", "invalid email", async () => {
  const result = await validateEmail(value);
  return result;
});

В данном случае возврат результата не требует явного создания промиса — функция уже возвращает его неявно.

Ключевой момент заключается в том, что любая ошибка, выброшенная внутри async функции, автоматически трансформируется в rejected промис:

test("password strength", "too weak password", async () => {
  const score = await calculateStrength(value);

  if (score < 3) {
    throw new Error("weak");
  }

  return true;
});

Параллельное выполнение асинхронных тестов

При проверке нескольких полей одновременно возникает проблема конкурентного исполнения. Валидационные промисы могут выполняться параллельно, но результат должен быть консистентным.

Типичная модель исполнения:

  • каждый тест запускается независимо
  • результаты собираются в агрегаторе состояния
  • финальная валидация производится после завершения всех промисов
const result = await validate(data);

console.log(result.hasErrors());

Если хотя бы один промис ещё находится в состоянии pending, результат не считается финальным.

Race conditions в асинхронной валидации

Одной из ключевых проблем является устаревание результатов. Если пользователь изменяет значение поля быстрее, чем завершается предыдущий промис, возникает ситуация гонки.

Пример:

let currentRequestId = 0;

test("username", "invalid", () => {
  const requestId = ++currentRequestId;

  return checkUsername(value).then((res) => {
    if (requestId !== currentRequestId) {
      return;
    }
    return res;
  });
});

Такая конструкция предотвращает применение устаревшего результата к актуальному состоянию поля.

В реальных системах валидации это часто решается через отмену запросов или игнорирование результатов по версии запроса.

Отмена промисов и управление жизненным циклом

Промисы сами по себе не отменяемы, поэтому управление их жизненным циклом требует дополнительных механизмов: AbortController или внутренние флаги состояния.

test("email", "invalid", ({ signal }) => {
  return fetch(`/api/check?email=${value}`, { signal })
    .then((r) => r.json());
});

При интеграции с системой валидации важно, чтобы отмена запроса не приводила к изменению состояния после завершения теста.

Агрегация асинхронных результатов

Валидационная система оперирует набором тестов, каждый из которых может возвращать промис. Результат агрегируется в единый объект состояния.

Логика объединения:

  • все синхронные тесты выполняются сразу
  • асинхронные помещаются в очередь
  • итог формируется после Promise.all над всеми тестами
const result = await Promise.all(validationTasks);

При этом важна деталь: ошибка одного промиса не должна прерывать выполнение остальных проверок, если система поддерживает полную диагностику.

Дебаунсинг и асинхронные тесты

Частое обновление данных приводит к множественным промисам, что перегружает систему. Для этого вводится механизм дебаунсинга асинхронных тестов.

const debouncedCheck = debounce((value) => {
  return checkEmail(value);
}, 300);

В контексте валидации это означает, что запуск промиса откладывается до стабилизации входного значения.

Комбинация синхронных и асинхронных тестов

Тестовая модель допускает смешивание синхронных и асинхронных правил. В этом случае система должна унифицировать результат в промис, чтобы обеспечить единый интерфейс обработки.

test("username", "required", () => {
  if (!value) return false;
});

test("username", "unique", async () => {
  return checkUnique(value);
});

Даже синхронный тест интерпретируется как мгновенно резолвящийся промис, что упрощает агрегирование.

Поток выполнения асинхронной валидации

Внутренний поток выполнения можно описать как последовательность стадий:

  • регистрация тестов
  • запуск синхронных проверок
  • инициация промисов асинхронных тестов
  • ожидание завершения всех промисов
  • агрегация результатов
  • формирование финального состояния

Каждый этап зависит от предыдущего, но асинхронные операции выполняются параллельно, что повышает производительность при большом числе проверок.

Ошибки в промисах и их нормализация

Любое исключение внутри промиса должно быть приведено к унифицированному виду ошибки. Это необходимо для того, чтобы система валидации могла агрегировать результаты без потери информации.

test("password", "invalid", async () => {
  try {
    return await validatePassword(value);
  } catch (e) {
    return Promise.reject("validation failed");
  }
});

Нормализация ошибок позволяет различать типы сбоев: сетевые, логические, пользовательские.

Повторное выполнение асинхронных тестов

При изменении входных данных все активные промисы должны быть либо завершены с игнорированием результата, либо пересозданы. Повторное выполнение является ключевым механизмом реактивной валидации.

function runValidation(data) {
  return validate(data);
}

Каждый вызов инициирует новый набор промисов, что исключает загрязнение состояния между итерациями.

Консистентность состояния при множественных промисах

Основная сложность асинхронной валидации заключается в необходимости поддерживать консистентность при одновременном выполнении нескольких промисов. Любое несогласованное завершение может привести к частично обновлённому состоянию.

Для этого используется правило:

  • состояние обновляется только после завершения всех активных промисов текущего цикла валидации

Это предотвращает появление промежуточных ошибок в пользовательском интерфейсе и обеспечивает атомарность результата проверки.