Unit-тесты для валидаторов

Unit-тестирование схем Joi строится вокруг проверки соответствия входных данных контракту, заданному через валидатор. Основная цель тестов — гарантировать, что каждая схема корректно принимает валидные данные и отклоняет некорректные во всех ожидаемых сценариях, включая граничные случаи и нестандартные входы.

Любая схема в Joi представляет собой набор правил валидации, который можно протестировать как обычную чистую функцию: на вход подаётся объект данных, на выходе получается либо результат с преобразованными значениями, либо ошибка валидации.

Ключевые свойства, которые проверяются в unit-тестах:

  • Корректное принятие валидных данных
  • Отклонение невалидных данных
  • Точность сообщений об ошибках
  • Стабильность поведения при одинаковом входе
  • Корректная работа преобразований (coerce/transform)

Настройка тестового окружения

Наиболее часто используется связка Jest или Mocha + Chai. В случае Jest дополнительная настройка минимальна.

Пример установки:

npm install --save-dev jest

Пример базовой структуры теста:

const Joi = require('joi');

const schema = Joi.object({
  name: Joi.string().min(3).required(),
  age: Joi.number().integer().min(18)
});

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

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

test('валидные данные проходят проверку', () => {
  const data = { name: 'Ivan', age: 25 };

  const { error, value } = schema.validate(data);

  expect(error).toBeUndefined();
  expect(value).toEqual(data);
});

Важно проверять не только отсутствие ошибки, но и итоговое значение, так как Joi может модифицировать данные (например, через trim, lowercase, default).

Проверка обязательных полей

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

test('отсутствие обязательного поля вызывает ошибку', () => {
  const data = { age: 25 };

  const { error } = schema.validate(data);

  expect(error).toBeDefined();
  expect(error.details[0].path).toContain('name');
});

Проверка path ошибки позволяет убедиться, что проблема относится к нужному полю.

Валидация типов данных

Joi строго различает типы, поэтому тестирование типизации критично.

test('неверный тип данных отклоняется', () => {
  const data = { name: 123, age: 'twenty' };

  const { error } = schema.validate(data);

  expect(error).toBeDefined();
});

Граничные значения

Граничные условия часто выявляют скрытые дефекты в схемах.

test('минимальная длина строки соблюдается', () => {
  const data = { name: 'Al', age: 20 };

  const { error } = schema.validate(data);

  expect(error).toBeDefined();
});
test('минимальное допустимое значение числа', () => {
  const data = { name: 'Alex', age: 18 };

  const { error } = schema.validate(data);

  expect(error).toBeUndefined();
});

Проверка вложенных структур

Сложные схемы часто содержат вложенные объекты и массивы.

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().required(),
    profile: Joi.object({
      email: Joi.string().email().required()
    })
  })
});

Тестирование:

test('вложенные структуры валидируются корректно', () => {
  const data = {
    user: {
      name: 'Ivan',
      profile: {
        email: 'ivan@mail.com'
      }
    }
  };

  const { error } = schema.validate(data);

  expect(error).toBeUndefined();
});

Негативный сценарий:

test('ошибка во вложенном поле фиксируется', () => {
  const data = {
    user: {
      name: 'Ivan',
      profile: {
        email: 'invalid-email'
      }
    }
  };

  const { error } = schema.validate(data);

  expect(error).toBeDefined();
  expect(error.details[0].path).toEqual(['user', 'profile', 'email']);
});

Тестирование массивов

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

const schema = Joi.object({
  tags: Joi.array().items(Joi.string().min(2))
});
test('массив валидных значений проходит проверку', () => {
  const data = { tags: ['js', 'node'] };

  const { error } = schema.validate(data);

  expect(error).toBeUndefined();
});
test('невалидный элемент массива вызывает ошибку', () => {
  const data = { tags: ['js', 123] };

  const { error } = schema.validate(data);

  expect(error).toBeDefined();
});

Проверка кастомных сообщений об ошибках

При использовании .messages() важно тестировать не только факт ошибки, но и её содержание.

const schema = Joi.string().min(5).messages({
  'string.min': 'Слишком короткая строка'
});
test('кастомное сообщение ошибки корректно возвращается', () => {
  const { error } = schema.validate('abc');

  expect(error.message).toBe('Слишком короткая строка');
});

Тестирование преобразований данных

Joi может модифицировать входные данные, что необходимо учитывать.

const schema = Joi.object({
  name: Joi.string().trim().lowercase()
});
test('данные преобразуются корректно', () => {
  const data = { name: '  IVAN  ' };

  const { value } = schema.validate(data);

  expect(value.name).toBe('ivan');
});

Асинхронная валидация

При использовании validateAsync тесты должны учитывать промисы.

test('асинхронная валидация проходит успешно', async () => {
  const data = { name: 'Ivan', age: 20 };

  const value = await schema.validateAsync(data);

  expect(value).toBeDefined();
});

Проверка ошибок:

test('асинхронная ошибка обрабатывается', async () => {
  expect.assertions(1);

  try {
    await schema.validateAsync({ name: 'Al' });
  } catch (err) {
    expect(err).toBeDefined();
  }
});

Параметризованные тесты

Для схожих сценариев удобно использовать табличные тесты.

test.each([
  [{ name: 'Al' }, false],
  [{ name: 'Alex' }, true],
  [{ name: '' }, false]
])('проверка имени %#', (input, expected) => {
  const { error } = schema.validate(input);

  expect(!error).toBe(expected);
});

Проверка устойчивости схем

Стабильность схемы означает, что одинаковый вход всегда даёт одинаковый результат.

test('детерминированность результата валидации', () => {
  const data = { name: 'Ivan', age: 25 };

  const first = schema.validate(data);
  const second = schema.validate(data);

  expect(first.value).toEqual(second.value);
  expect(first.error).toEqual(second.error);
});

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

Схемы должны тестироваться как изолированные единицы без зависимости от внешнего состояния. Это означает:

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

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

const schema = Joi.object({
  id: Joi.string().external(async (value) => {
    return value;
  })
});

Частые ошибки при тестировании Joi

  • Проверка только наличия ошибки без анализа details
  • Игнорирование преобразований значений
  • Отсутствие тестов на граничные значения
  • Непроверенные вложенные структуры
  • Отсутствие проверки массивов и их элементов
  • Игнорирование асинхронной валидации

Организация тестового покрытия

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

  • базовые поля
  • обязательность
  • типы
  • граничные значения
  • вложенные объекты
  • массивы
  • кастомные правила
  • трансформации
  • асинхронные проверки

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