Частые ошибки

Нарушение принципа изоляции тестов

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

Некорректный пример:

import { create, test } from 'vest';

const suite = create((data) => {
  let isPasswordValid = false;

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

    enforce(isPasswordValid);
  });

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

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

Правильный вариант:

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

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

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

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


Использование побочных эффектов внутри test

Vest предполагает декларативный стиль валидации. Побочные эффекты внутри test приводят к трудноуловимым ошибкам.

Плохой подход:

test('email', 'Email занят', async () => {
  analytics.track('email_validation');

  const result = await api.checkEmail();

  enforce(result.available);
});

Проблемы:

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

Корректнее выносить побочные эффекты за пределы suite.


Повторное создание suite при каждом рендере

Очень частая ошибка в React-приложениях — объявление create() внутри компонента.

Неправильно:

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

  return null;
}

При каждом рендере создаётся новый экземпляр suite, из-за чего:

  • теряется состояние;
  • ломается кеширование;
  • сбрасываются async-проверки;
  • возникают проблемы с производительностью.

Правильная практика:

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

Suite должен создаваться один раз.


Неверное использование async-валидации

Vest поддерживает асинхронные тесты, но ошибки в их использовании встречаются постоянно.

Некорректный пример:

test('email', 'Email уже существует', () => {
  fetch('/api/check-email');
});

Vest считает тест синхронным, потому что Promise не возвращается.

Правильно:

test('email', 'Email уже существует', async () => {
  const response = await fetch('/api/check-email');
  const result = await response.json();

  enforce(result.available);
});

Либо:

test('email', 'Email уже существует', () => {
  return fetch('/api/check-email')
    .then(res => res.json())
    .then(result => {
      enforce(result.available);
    });
});

Отсутствие enforce в тесте

Иногда разработчики используют test как обычный callback.

Ошибка:

test('age', 'Возраст некорректен', () => {
  data.age > 18;
});

Такой тест всегда считается успешным, потому что отсутствует assertion.

Правильно:

test('age', 'Возраст некорректен', () => {
  enforce(data.age > 18);
});

Или:

test('age', 'Возраст некорректен', () => {
  enforce(data.age).greaterThan(18);
});

Смешивание бизнес-логики и валидации

Vest предназначен для проверки данных, а не для выполнения бизнес-операций.

Антипаттерн:

test('payment', 'Ошибка оплаты', async () => {
  await paymentService.chargeCard();
});

Валидация не должна:

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

Корректная задача suite — только определить валидность данных.


Избыточная вложенность условий

Плохо структурированные проверки ухудшают читаемость.

Сложный вариант:

test('password', 'Некорректный пароль', () => {
  if (data.password) {
    if (data.password.length > 7) {
      if (/[A-Z]/.test(data.password)) {
        if (/[0-9]/.test(data.password)) {
          return;
        }
      }
    }
  }

  enforce(false);
});

Лучше использовать небольшие независимые проверки:

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

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

test('password', 'Нужна заглавная буква', () => {
  enforce(/[A-Z]/.test(data.password));
});

test('password', 'Нужна цифра', () => {
  enforce(/[0-9]/.test(data.password));
});

Преимущества:

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

Игнорирование only и skip

Vest предоставляет механизмы управления выполнением тестов.

Ошибка — ручное управление через if.

Плохой пример:

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

Правильнее:

import { skipWhen } from 'vest';

skipWhen(!shouldValidateEmail, () => {
  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(emailRegex);
  });
});

Либо:

only('email');

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


Неверная работа с optional-полями

Частая ошибка — обязательная проверка необязательных полей.

Некорректно:

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

Если поле пустое, тест упадёт.

Корректный вариант:

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

  enforce(data.middleName.length >= 2);
});

Дублирование логики

Повторяющиеся проверки быстро усложняют кодовую базу.

Плохой пример:

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

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

Лучше выносить общие проверки:

function validateEmail(value) {
  enforce(value).matches(emailRegex);
}

test('email', 'Некорректный email', () => {
  validateEmail(data.email);
});

test('backupEmail', 'Некорректный email', () => {
  validateEmail(data.backupEmail);
});

Слишком большие suite

Огромные suite становятся трудно поддерживаемыми.

Признаки проблемы:

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

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

const suite = create((data) => {
  // регистрация
  // профиль
  // настройки
  // платежи
  // уведомления
});

Лучше разделять ответственность:

export const registrationSuite = create(...);
export const profileSuite = create(...);
export const paymentSuite = create(...);

Неправильное использование warning

Некоторые разработчики используют warning как полноценную ошибку.

Неверно:

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

Warning не блокирует форму.

Для обязательных полей нужен test:

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

warning подходит для:

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

Отсутствие группировки проверок

При большом количестве тестов без структуры код становится нечитаемым.

Плохо:

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

Лучше логически группировать:

// username
test(...);
test(...);

// password
test(...);
test(...);

// profile
test(...);
test(...);

Проверка undefined без защиты

Частая причина runtime-ошибок:

test('tags', 'Минимум один тег', () => {
  enforce(data.tags.length > 0);
});

Если tags отсутствует:

Cannot read property 'length' of undefined

Правильно:

test('tags', 'Минимум один тег', () => {
  enforce(Array.isArray(data.tags));
  enforce(data.tags.length > 0);
});

Использование исключений вместо enforce

Некоторые разработчики пытаются выбрасывать ошибки вручную.

Плохой вариант:

test('username', 'Ошибка', () => {
  if (!data.username) {
    throw new Error('Username required');
  }
});

Vest не предназначен для такого сценария.

Правильно:

test('username', 'Username required', () => {
  enforce(data.username).isNotBlank();
});

Неверная работа с динамическими полями

При работе со списками часто возникают проблемы с идентификацией полей.

Некорректно:

data.users.forEach(user => {
  test('email', 'Некорректный email', () => {
    enforce(user.email).matches(emailRegex);
  });
});

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

Корректный подход:

data.users.forEach((user, index) => {
  test(`users.${index}.email`, 'Некорректный email', () => {
    enforce(user.email).matches(emailRegex);
  });
});

Полная блокировка формы из-за async-проверок

Иногда асинхронные проверки делают интерфейс медленным.

Плохой пример:

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

  enforce(result.available);
});

Если запрос выполняется на каждый ввод символа, возникают:

  • перегрузка API;
  • лаги интерфейса;
  • гонки запросов.

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

  • debounce;
  • кеширование;
  • проверка только после blur;
  • отмена старых запросов.

Игнорирование результатов suite

Иногда suite вызывается, но результат не анализируется.

Ошибка:

suite(data);
submitForm();

Правильно:

const result = suite(data);

if (result.hasErrors()) {
  return;
}

submitForm();

Непонимание различий между hasErrors и getErrors

Ошибка использования API:

if (result.getErrors()) {
  return;
}

getErrors() возвращает объект, который всегда truthy.

Правильно:

if (result.hasErrors()) {
  return;
}

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

const errors = result.getErrors();

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

result.hasErrors();

Состояние вне suite

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

Плохой пример:

let validationCounter = 0;

const suite = create((data) => {
  validationCounter++;

  test('field', 'Ошибка', () => {
    enforce(data.field).isNotBlank();
  });
});

Такой код приводит к трудноуловимым багам, особенно в React Strict Mode.

Suite должен быть максимально чистым и детерминированным.


Неправильное именование полей

Слишком общие имена усложняют поддержку.

Плохо:

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

Лучше:

test('billing.address.street', 'Улица обязательна', () => {});

Хорошие имена полей:

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

Избыточное количество async-тестов

Не каждая проверка должна быть асинхронной.

Плохой пример:

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

Async создаёт дополнительную нагрузку.

Синхронные проверки должны оставаться синхронными:

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

Игнорирование читаемости сообщений об ошибках

Плохие сообщения:

test('password', 'invalid', () => {});

Либо:

test('password', 'Error 123', () => {});

Хорошие сообщения:

test('password', 'Пароль должен содержать минимум 8 символов', () => {});

Качественное сообщение:

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