Функция test

Функция test — базовый строительный блок библиотеки Vest. Именно через неё описываются отдельные проверки данных формы, состояния или бизнес-логики. Каждый вызов test создаёт независимый сценарий валидации, который Vest отслеживает, выполняет и сохраняет в результирующем состоянии suite.

Сигнатура функции:

test(fieldName, message, callback);

Базовая структура:

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

const suite = create((data = {}) => {
  test('username', 'Имя пользователя обязательно', () => {
    enforce(data.username).isNotBlank();
  });
});

Аргументы функции

fieldName

Имя поля, к которому относится проверка.

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

Vest использует это имя:

  • для группировки ошибок;
  • для получения статуса поля;
  • для выборочной валидации;
  • для построения UI-логики.

Допускается использовать любые строковые идентификаторы:

test('profile.email', 'Ошибка email', () => {});
test('address.city', 'Город обязателен', () => {});

message

Сообщение ошибки, которое будет возвращено при неуспешной проверке.

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

Сообщение может быть:

  • статическим;
  • динамически вычисляемым;
  • локализованным.

Пример динамического сообщения:

test(
  'age',
  `Возраст должен быть больше ${minAge}`,
  () => {
    enforce(data.age).greaterThan(minAge);
  }
);

callback

Функция с логикой проверки.

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

Если внутри callback возникает ошибка enforcement, тест считается проваленным.


Простая валидация поля

Проверка обязательного значения

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

Проверка длины строки

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

Проверка числового диапазона

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

Несколько тестов для одного поля

Одно поле может иметь несколько независимых тестов.

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

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

test('password', 'Пароль должен содержать цифру', () => {
  enforce(data.password).matches(/\d/);
});

Vest сохраняет все ошибки отдельно.

Результат:

[
  'Пароль обязателен',
  'Минимум 8 символов',
  'Пароль должен содержать цифру'
]

Асинхронные проверки

test поддерживает асинхронную валидацию.

Проверка уникальности email

test(
  'email',
  'Email уже используется',
  async () => {
    const response = await fetch('/api/check-email');

    const result = await response.json();

    enforce(result.available).isTruthy();
  }
);

Vest автоматически отслеживает:

  • pending-состояние;
  • завершение проверки;
  • результаты асинхронных тестов.

Проверка через API

test(
  'username',
  'Имя пользователя занято',
  async () => {
    const response = await api.userExists(data.username);

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

Исключения внутри test

Vest воспринимает ошибку как неуспешную проверку.

test('price', 'Некорректная цена', () => {
  if (data.price < 0) {
    throw new Error();
  }
});

Однако предпочтительнее использовать enforce, поскольку он создаёт предсказуемую и стандартизированную логику.


Работа с enforce

Функция test почти всегда используется вместе с enforce.

Проверка строки

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

Проверка массива

test('tags', 'Добавьте хотя бы один тег', () => {
  enforce(data.tags).isArray();
  enforce(data.tags.length).greaterThan(0);
});

Проверка объекта

test('settings', 'Настройки обязательны', () => {
  enforce(data.settings).isObject();
});

Условная валидация

Проверки можно выполнять только при определённых условиях.

Проверка поля при наличии значения

test('middleName', 'Минимум 2 символа', () => {
  if (!data.middleName) {
    return;
  }

  enforce(data.middleName).longerThanOrEquals(2);
});

Зависимая валидация

test(
  'confirmPassword',
  'Пароли не совпадают',
  () => {
    enforce(data.confirmPassword).equals(data.password);
  }
);

Динамическое создание тестов

Валидация массива полей

const requiredFields = [
  'firstName',
  'lastName',
  'email'
];

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

Генерация тестов из конфигурации

const rules = [
  {
    field: 'username',
    min: 4
  },
  {
    field: 'password',
    min: 8
  }
];

rules.forEach(rule => {
  test(
    rule.field,
    `Минимум ${rule.min} символов`,
    () => {
      enforce(data[rule.field])
        .longerThanOrEquals(rule.min);
    }
  );
});

Выполнение тестов

Создание suite

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

Запуск проверки

const result = suite({
  email: ''
});

Получение ошибок

result.getErrors('email');

Результат:

['Введите email']

Статусы тестов

Vest хранит состояние каждого test.

Проверка наличия ошибок

result.hasErrors('email');

Проверка pending-состояния

result.isPending('email');

Проверка успешной валидации

result.isValid('email');

Валидация только изменённого поля

Одно из ключевых преимуществ Vest — selective validation.

const suite = create((data, fieldName) => {
  only(fieldName);

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

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

Запуск:

suite(formData, 'email');

В этом случае выполнится только тест поля email.


Группировка логики тестов

Вынесение проверки в функцию

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

  test('email', 'Некорректный формат', () => {
    enforce(email).matches(/.+@.+/);
  });
}

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

const suite = create(data => {
  validateEmail(data.email);
});

Тесты внутри циклов

Vest корректно работает с динамическими структурами.

Проверка списка товаров

data.items.forEach((item, index) => {
  test(
    `items.${index}.title`,
    'Название обязательно',
    () => {
      enforce(item.title).isNotBlank();
    }
  );
});

Асинхронные ошибки и race conditions

Vest умеет предотвращать проблемы устаревших запросов.

Пример

test(
  'username',
  'Имя пользователя занято',
  async () => {
    const result = await api.checkUsername(
      data.username
    );

    enforce(result.available).isTruthy();
  }
);

Если пользователь быстро изменяет значение поля:

alex
alex1
alex12
alex123

Vest игнорирует устаревшие результаты предыдущих запросов.


Комбинация с skip

Пропуск теста

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

Комбинация с omitWhen

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

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

test может создавать предупреждения вместо ошибок.

import { warn } from 'vest';

warn(
  'password',
  'Пароль слишком простой',
  () => {
    enforce(data.password)
      .matches(/[A-Z]/);
  }
);

Предупреждения:

  • не блокируют форму;
  • сохраняются отдельно;
  • полезны для UX-подсказок.

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

Слишком много логики в одном test

Плохо:

test('email', 'Ошибка email', () => {
  enforce(data.email).isNotBlank();
  enforce(data.email).matches(/.+@.+/);
  enforce(data.email.length).lessThan(50);
});

Недостатки:

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

Лучше:

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

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

test('email', 'Максимум 50 символов', () => {
  enforce(data.email.length).lessThan(50);
});

Побочные эффекты внутри test

Нежелательно:

test('email', 'Ошибка', () => {
  saveUser();
});

Тесты должны быть детерминированными и не изменять состояние приложения.


Использование случайных значений

Плохо:

test('token', 'Ошибка', () => {
  enforce(Math.random()).greaterThan(0.5);
});

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


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

Один test — одна ответственность

Хорошая практика:

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

test('phone', 'Некорректный формат телефона', () => {
  enforce(data.phone).matches(phoneRegex);
});

Плоская структура тестов

Vest лучше читается при линейной организации.

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

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


Использование переиспользуемых валидаторов

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

Применение:

required('email', data.email);
required('password', data.password);

Внутренний механизм работы test

При выполнении test библиотека:

  1. Регистрирует проверку в текущем suite.

  2. Привязывает тест к указанному полю.

  3. Выполняет callback.

  4. Отслеживает синхронный или асинхронный результат.

  5. Сохраняет статус:

    • valid;
    • invalid;
    • pending;
    • warning.
  6. Обновляет aggregate state suite.

Именно поэтому test является центральным элементом всей архитектуры Vest.