Coverage валидационной логики

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

Валидационная система Vest строится на декларативных блоках, где каждая проверка может включать:

  • простые предикаты (equals, isEmpty, matches)
  • условные правила (when, skip)
  • группировку (group)
  • асинхронные проверки
  • зависимые поля

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

Простейший пример:

import { test, enforce } from 'vest';

const suite = (data) => {
  test('username', 'required', () => {
    enforce(data.username).isNotEmpty();
  });

  test('username', 'min_length', () => {
    enforce(data.username).longerThanOrEquals(3);
  });
};

Минимальное покрытие здесь включает:

  • пустое значение
  • строку длиной < 3
  • корректное значение ≥ 3 символов

Отсутствие любого из этих сценариев создаёт неполное логическое покрытие.

Ветвление через условные правила

Условные проверки создают экспоненциальный рост комбинаций, которые необходимо учитывать.

import { test, when, enforce } from 'vest';

const suite = (data) => {
  when(data.role === 'admin', () => {
    test('accessCode', 'required_for_admin', () => {
      enforce(data.accessCode).isNotEmpty();
    });
  });
};

Здесь возникает минимум два состояния:

  • role === 'admin'
  • role !== 'admin'

Но для покрытия логики важно учитывать также:

  • admin без accessCode
  • admin с accessCode
  • non-admin с accessCode
  • non-admin без accessCode

Даже если последний вариант не вызывает проверок, он важен для покрытия ветки when.

Покрытие групповой валидации

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

import { group, test, enforce } from 'vest';

group('auth', () => {
  test('email', 'invalid_email', () => {
    enforce(data.email).matches(/.+@.+/);
  });

  test('password', 'weak_password', () => {
    enforce(data.password).longerThan(8);
  });
});

Для полного покрытия необходимо учитывать:

  • валидные email / валидные password
  • невалидный email / валидный password
  • валидный email / невалидный password
  • оба невалидны

Дополнительно важно учитывать агрегированное состояние группы:

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

Асинхронные валидаторы и их покрытие

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

test('email', 'email_taken', async () => {
  await enforce(data.email).isNotTaken();
});

Минимальный набор сценариев:

  • email свободен (resolve true)
  • email занят (resolve false)
  • ошибка сети (reject)
  • таймаут (если предусмотрен механизм)

Важно различать логическое покрытие и покрытие состояний выполнения:

  • success path
  • failure path
  • exception path

Недостаточное покрытие асинхронных сценариев часто приводит к ложноположительным результатам тестов.

Влияние skip и динамического отключения правил

Динамическое отключение правил через skip или условные конструкции изменяет структуру дерева валидации.

test('phone', 'invalid', () => {
  if (data.country !== 'KZ') {
    return;
  }
  enforce(data.phone).matches(/^\d+$/);
});

Здесь покрытие должно включать:

  • country === ‘KZ’ и валидный телефон
  • country === ‘KZ’ и невалидный телефон
  • country !== ‘KZ’ (ветка пропуска)

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

Комбинаторное покрытие зависимых полей

Связанные поля создают мультипликативное увеличение количества сценариев.

test('password_confirm', 'mismatch', () => {
  enforce(data.passwordConfirm).equals(data.password);
});

Для покрытия необходимо учитывать:

  • password === passwordConfirm (валидно)
  • password !== passwordConfirm (невалидно)
  • оба поля пустые
  • одно поле пустое

При наличии нескольких зависимых полей (например, email + confirm email + recovery email) пространство состояний становится комбинаторным.

Метрики покрытия валидационной логики

Классические метрики (line coverage, branch coverage) недостаточны для Vest-подобных систем. Более релевантные метрики:

Branch coverage validation rules

  • покрытие каждого условия when
  • покрытие всех исходов enforce

State coverage

  • набор входных данных → итоговый набор ошибок

Rule activation coverage

  • каждая проверка test была активирована хотя бы один раз

Error-path coverage

  • каждая ошибка была сгенерирована хотя бы в одном сценарии

Edge cases как обязательная часть покрытия

Типовые пропуски в тестировании валидации:

  • undefined вместо строки
  • null значения
  • строки с пробелами " "
  • unicode-символы
  • чрезмерно длинные строки
  • типы данных вне ожидаемого диапазона
enforce(data.username).isNotEmpty();

Данный вызов требует покрытия:

  • ''
  • ' '
  • null
  • undefined
  • 'a'

Каждый из этих случаев может вести к разному поведению в зависимости от реализации enforce-логики.

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

В сложных схемах важно фиксировать неизменные свойства:

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

Нарушение этих инвариантов обычно указывает на пробелы в покрытии, а не в логике.

Стратегия построения полного покрытия

Практически полное покрытие достигается через разбиение логики на уровни:

  1. Базовые предикаты (isEmpty, isNotEmpty)
  2. Условные ветвления (when)
  3. Зависимые поля
  4. Асинхронные проверки
  5. Групповые сценарии
  6. Краевые случаи входных данных

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