Изоляция тестов

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

Состояние выполнения и его источники

Внутреннее состояние системы валидации формируется из нескольких источников:

  • зарегистрированные группы проверок (group)
  • наборы тестов (test)
  • результаты предыдущего выполнения suite
  • асинхронные операции и их кэшированные результаты
  • внешние зависимости (API, моки, сторы)

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

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


Проблема разделяемого состояния

Типичная ошибка при работе с валидационными suite — повторное использование одного и того же экземпляра без очистки состояния:

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

Такие эффекты особенно заметны в тестовых раннерах, где suite вызывается многократно в рамках одного процесса.


Механизм изоляции через создание новых экземпляров

Базовый принцип изоляции в Vest заключается в том, что каждый запуск должен опираться на независимую функцию или фабрику suite.

import { create, test } from "vest";

const suiteFactory = (data) => create("user_form", () => {

  test("username", "Имя обязательно", () => {
    if (!data.username) {
      return false;
    }
  });

});

Каждый вызов suiteFactory создаёт новый контекст выполнения, исключая утечку предыдущих результатов.


Поведение глобального и локального состояния

Внутренняя архитектура Vest разделяет:

  • глобальные идентификаторы suite
  • локальные runtime-данные конкретного запуска

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

Однако при повторном вызове без пересоздания suite возникает риск повторного использования runtime-контекста.


Очистка состояния между запусками

Для сценариев, где suite используется повторно (например, в UI-валидации), применяется явная очистка состояния.

import { create, test, reset } from "vest";

const suite = create("profile", (data) => {

  test("email", "Неверный email", () => {
    if (!data.email.includes("@")) {
      return false;
    }
  });

});

reset("profile");
suite({ email: "test" });

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


Изоляция в асинхронных тестах

Асинхронные проверки создают дополнительный уровень сложности, поскольку их выполнение не синхронизировано с основным потоком suite.

import { test, create } from "vest";

const suite = create("async_suite", (data) => {

  test("username", "Проверка занятости", async () => {
    const res = await fetch(`/api/check?u=${data.username}`);
    const json = await res.json();

    if (!json.available) {
      return false;
    }
  });

});

При отсутствии изоляции возможны следующие проблемы:

  • завершение старого запроса перезаписывает новый результат
  • гонки между несколькими вызовами suite
  • неконсистентные состояния validation result

Решение заключается в том, что каждый запуск suite должен иметь уникальный execution context, а результаты асинхронных операций должны привязываться к идентификатору запуска.


Изоляция через идентификатор выполнения

Каждый запуск suite концептуально можно рассматривать как отдельную транзакцию.

  • запуск A → executionId: 1
  • запуск B → executionId: 2

Любой результат, пришедший после завершения executionId 1, игнорируется, если активен executionId 2.

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


Поведение групп и вложенных контекстов

Группы (group) в Vest используются для объединения тестов по логическим блокам. Однако при неправильном использовании они могут стать источником скрытого состояния.

import { create, group, test } from "vest";

const suite = create("form", (data) => {

  group("credentials", () => {

    test("login", "Логин обязателен", () => {
      if (!data.login) return false;
    });

    test("password", "Пароль обязателен", () => {
      if (!data.password) return false;
    });

  });

});

Каждый group должен быть пересоздан при новом запуске suite. Если группа сохраняет внутренний state между вызовами, тесты начинают «перекрёстно загрязнять» результаты.


Параллельные вызовы suite и race condition

При частом вызове suite, например при вводе в форму, возможна ситуация:

  1. запуск A стартует проверку email
  2. запуск B стартует проверку email
  3. запуск B завершается быстрее
  4. запуск A завершает async проверку позже и перезаписывает результат

Это классическая гонка состояний.

Изоляция решает эту проблему через:

  • отмену устаревших результатов
  • привязку тестов к execution context
  • игнорирование поздних ответов

Кэширование и его влияние на изоляцию

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

  • входные данные не учитываются как часть ключа кэша
  • тест зависит от внешнего состояния
  • suite не пересоздаётся между запусками

Для сохранения изоляции кэш должен быть либо:

  • строго привязан к входным данным
  • либо отключён в тестовой среде

Интеграция изоляции с UI-слоями

При использовании Vest в интерфейсных приложениях (React, Vue или аналогичных) важно учитывать жизненный цикл компонентов.

Основные источники нарушения изоляции:

  • повторное использование suite вне компонента
  • хранение результата в глобальной переменной
  • отсутствие очистки при размонтировании

Правильная модель предполагает, что suite вызывается как чистая функция от состояния UI:

const result = suite(formState);

Любая привязка к внешнему mutable state увеличивает риск утечки между рендерами.


Изоляция и детерминированность результата

Идеальная изоляция приводит к детерминированному поведению:

  • одинаковые входные данные → одинаковый результат
  • отсутствие зависимости от порядка вызовов
  • отсутствие влияния предыдущих запусков

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


Поведение при частичных обновлениях данных

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

suite({ username: "alex" });
suite({ username: "alex", email: "a@b.com" });

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

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


Разделение runtime и декларативного слоя

Архитектура Vest предполагает разделение:

  • декларативного слоя (описание правил)
  • runtime слоя (результаты выполнения)

Изоляция достигается только тогда, когда runtime слой не переживает границы одного запуска suite.

Любая попытка кэшировать runtime вне контекста executionId приводит к нарушению консистентности.


Стратегии обеспечения строгой изоляции

На практике применяются несколько устойчивых стратегий:

  • создание нового suite на каждый запуск
  • явный reset состояния перед повторным использованием
  • отказ от глобального хранения результатов
  • привязка async операций к execution context
  • игнорирование устаревших async результатов

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