Группы тестов

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

Группы позволяют:

  • структурировать крупные validation suites;
  • запускать проверки выборочно;
  • разделять правила по контекстам;
  • управлять производительностью;
  • создавать независимые секции формы;
  • реализовывать пошаговые формы и динамические сценарии.

Основной механизм группировки строится вокруг функции group().


Базовый синтаксис group

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

const suite = create((data) => {

  group('credentials', () => {

    test('email', 'Некорректный email', () => {
      enforce(data.email).matches(/^\S+@\S+\.\S+$/);
    });

    test('password', 'Пароль слишком короткий', () => {
      enforce(data.password).longerThanOrEquals(8);
    });

  });

});

В данном примере обе проверки входят в группу credentials.

Имя группы:

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

Как работает группировка

Функция group() создаёт контекст выполнения. Все тесты внутри callback автоматически связываются с указанной группой.

group('profile', () => {
  // тесты принадлежат группе profile
});

Группа не изменяет сами тесты. Она лишь добавляет дополнительный уровень организации.


Несколько групп в одном suite

Крупные формы обычно делятся на смысловые секции.

const suite = create((data) => {

  group('account', () => {

    test('email', 'Email обязателен', () => {
      enforce(data.email).isNotBlank();
    });

    test('password', 'Минимум 8 символов', () => {
      enforce(data.password).longerThanOrEquals(8);
    });

  });

  group('profile', () => {

    test('firstName', 'Введите имя', () => {
      enforce(data.firstName).isNotBlank();
    });

    test('age', 'Возраст должен быть положительным', () => {
      enforce(data.age).greaterThan(0);
    });

  });

});

Такой подход:

  • улучшает читаемость;
  • упрощает поддержку;
  • позволяет независимо запускать секции;
  • уменьшает связанность логики.

Повторное использование имени группы

Одна и та же группа может быть объявлена несколько раз.

group('profile', () => {

  test('firstName', 'Введите имя', () => {
    enforce(data.firstName).isNotBlank();
  });

});

group('profile', () => {

  test('lastName', 'Введите фамилию', () => {
    enforce(data.lastName).isNotBlank();
  });

});

Все тесты объединяются в одну логическую группу profile.

Это особенно полезно при:

  • разделении кода по модулям;
  • использовании helper-функций;
  • композиции validation logic.

Группы и условная логика

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

Проверка юридического лица

const suite = create((data) => {

  test('type', 'Укажите тип аккаунта', () => {
    enforce(data.type).inside(['person', 'company']);
  });

  if (data.type === 'company') {

    group('company', () => {

      test('companyName', 'Введите название компании', () => {
        enforce(data.companyName).isNotBlank();
      });

      test('taxId', 'Введите ИНН', () => {
        enforce(data.taxId).isNotBlank();
      });

    });

  }

});

Группа company будет существовать только для корпоративных аккаунтов.


Группы в многошаговых формах

Одна из самых популярных задач — wizard forms.

Шаг 1 — аккаунт

group('step1', () => {

  test('email', 'Введите email', () => {
    enforce(data.email).isNotBlank();
  });

  test('password', 'Введите пароль', () => {
    enforce(data.password).longerThanOrEquals(8);
  });

});

Шаг 2 — профиль

group('step2', () => {

  test('firstName', 'Введите имя', () => {
    enforce(data.firstName).isNotBlank();
  });

  test('lastName', 'Введите фамилию', () => {
    enforce(data.lastName).isNotBlank();
  });

});

Шаг 3 — адрес

group('step3', () => {

  test('country', 'Укажите страну', () => {
    enforce(data.country).isNotBlank();
  });

  test('city', 'Укажите город', () => {
    enforce(data.city).isNotBlank();
  });

});

Подобная организация:

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

Запуск определённых групп

Vest позволяет запускать только нужные группы.

Частичный запуск suite

suite(data, {
  group: 'step2'
});

Теперь выполняются только тесты группы step2.

Это особенно важно для:

  • больших форм;
  • SPA-приложений;
  • динамических интерфейсов;
  • lazy validation.

Выполнение нескольких групп

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

suite(data, {
  group: ['step1', 'step2']
});

Vest выполнит обе группы.

Подход полезен при:

  • комбинированной валидации;
  • повторной проверке зависимых шагов;
  • серверной валидации;
  • промежуточном сохранении формы.

Группы и производительность

При большом количестве тестов selective validation существенно уменьшает нагрузку.

Без групп

suite(data);

Выполняются все проверки.

С группами

suite(data, {
  group: 'payment'
});

Запускается только необходимая часть validation suite.

На крупных enterprise-формах разница может быть очень заметной.


Вложенные группы

Vest поддерживает вложенную структуру.

group('checkout', () => {

  group('delivery', () => {

    test('address', 'Введите адрес', () => {
      enforce(data.address).isNotBlank();
    });

  });

  group('payment', () => {

    test('card', 'Введите номер карты', () => {
      enforce(data.card).isNotBlank();
    });

  });

});

Такая структура помогает моделировать сложную предметную область.


Семантическая организация

Группы не обязательно должны соответствовать UI.

Пример доменной структуры

group('security', () => {

  test('password', 'Слабый пароль', () => {
    enforce(data.password).longerThanOrEquals(12);
  });

  test('twoFactor', 'Требуется 2FA', () => {
    enforce(data.twoFactor).isTruthy();
  });

});

Здесь группа отражает бизнес-смысл, а не визуальный блок интерфейса.


Группы и переиспользуемые функции

Validation logic часто выносится в отдельные модули.

Helper для профиля

function validateProfile(data) {

  group('profile', () => {

    test('firstName', 'Введите имя', () => {
      enforce(data.firstName).isNotBlank();
    });

    test('lastName', 'Введите фамилию', () => {
      enforce(data.lastName).isNotBlank();
    });

  });

}

Использование

const suite = create((data) => {

  validateProfile(data);

});

Такой подход:

  • уменьшает дублирование;
  • улучшает масштабируемость;
  • упрощает поддержку больших проектов.

Динамические группы

Имя группы может формироваться программно.

group(`step_${currentStep}`, () => {

  test('value', 'Поле обязательно', () => {
    enforce(data.value).isNotBlank();
  });

});

Однако чрезмерная динамика способна усложнить отладку.

Обычно рекомендуется:

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

Группы и асинхронные проверки

Группы работают и с async validation.

group('account', () => {

  test(
    'email',
    'Email уже используется',
    async () => {

      const exists = await api.emailExists(data.email);

      enforce(exists).isFalsy();
    }
  );

});

Изоляция асинхронных тестов по группам особенно полезна при:

  • серверной проверке;
  • debounce validation;
  • проверке уникальности;
  • expensive operations.

Комбинация групп и only

Vest поддерживает selective field validation через only().

Группы и only() можно комбинировать.

suite(data, {
  only: ['email'],
  group: 'account'
});

В этом случае:

  • выполняется только группа account;
  • внутри неё запускаются лишь тесты поля email.

Группы и skip

Аналогично можно исключать проверки.

suite(data, {
  skip: ['taxId'],
  group: 'company'
});

Проверки поля taxId будут пропущены.


Организация больших validation suites

Плохая структура

create((data) => {

  test(...);
  test(...);
  test(...);
  test(...);
  test(...);
  test(...);
  test(...);

});

Проблемы:

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

Хорошая структура

create((data) => {

  group('account', () => {
    // account validation
  });

  group('profile', () => {
    // profile validation
  });

  group('security', () => {
    // security validation
  });

  group('billing', () => {
    // billing validation
  });

});

Такой подход значительно улучшает архитектуру validation layer.


Паттерн feature-based validation

В крупных приложениях validation удобно строить вокруг feature-модулей.

Структура

validation/
  account/
  billing/
  profile/
  security/

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

export function billingValidation(data) {

  group('billing', () => {

    test('cardNumber', 'Введите карту', () => {
      enforce(data.cardNumber).isNotBlank();
    });

  });

}

Группы и переиспользование между формами

Один и тот же validation block можно использовать повторно.

function addressGroup(data) {

  group('address', () => {

    test('city', 'Введите город', () => {
      enforce(data.city).isNotBlank();
    });

    test('street', 'Введите улицу', () => {
      enforce(data.street).isNotBlank();
    });

  });

}

Применение:

create((data) => {

  addressGroup(data);

});

Типичные ошибки

Слишком крупные группы

Плохо:

group('form', () => {
  // 200 тестов
});

Лучше:

group('profile', () => {});
group('billing', () => {});
group('security', () => {});

Отсутствие семантики

Плохо:

group('g1', () => {});
group('g2', () => {});

Лучше:

group('shipping', () => {});
group('payment', () => {});

Чрезмерная вложенность

Плохо:

group('a', () => {
  group('b', () => {
    group('c', () => {
      group('d', () => {

      });
    });
  });
});

Глубокая вложенность затрудняет понимание validation flow.


Практический пример полной структуры

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

export const registrationSuite = create((data) => {

  group('account', () => {

    test('email', 'Введите email', () => {
      enforce(data.email).isNotBlank();
    });

    test('password', 'Минимум 8 символов', () => {
      enforce(data.password).longerThanOrEquals(8);
    });

  });

  group('profile', () => {

    test('firstName', 'Введите имя', () => {
      enforce(data.firstName).isNotBlank();
    });

    test('lastName', 'Введите фамилию', () => {
      enforce(data.lastName).isNotBlank();
    });

  });

  group('agreement', () => {

    test('terms', 'Необходимо согласие', () => {
      enforce(data.terms).isTruthy();
    });

  });

});

Валидация только аккаунта

registrationSuite(data, {
  group: 'account'
});

Валидация профиля и соглашений

registrationSuite(data, {
  group: ['profile', 'agreement']
});

Архитектурные рекомендации

Использовать группы как границы ответственности

Каждая группа должна представлять:

  • отдельный шаг;
  • feature-модуль;
  • бизнес-сущность;
  • изолированную секцию формы.

Не смешивать unrelated validation

Плохо:

group('misc', () => {
  // всё подряд
});

Хорошо:

group('payment', () => {});
group('shipping', () => {});
group('notifications', () => {});

Делать имена стабильными

Имена групп становятся частью архитектуры приложения.

Желательно:

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

Группы как инструмент композиции

В крупных проектах группы превращаются в строительные блоки validation architecture.

Они позволяют:

  • собирать suite из независимых модулей;
  • включать и отключать validation sections;
  • оптимизировать выполнение;
  • изолировать сложные проверки;
  • строить масштабируемую структуру;
  • разделять ответственность между командами и feature-модулями.