Юнит-тестирование схем

Юнит-тестирование схем Yup обеспечивает контроль корректности правил валидации на уровне изолированных модулей, где каждая схема рассматривается как самостоятельная логическая единица. Такой подход снижает риск регрессий при изменении правил валидации и упрощает сопровождение формальных контрактов данных в приложении.

Схемы Yup часто выступают единственным источником истины для структуры входных данных. Они определяют:

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

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

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

Для тестирования схем Yup обычно используются фреймворки Jest или Vitest.

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

npm install yup
npm install jest

Минимальная конфигурация теста:

import * as yup from 'yup';

Асинхронная природа Yup требует работы с await, так как метод validate возвращает Promise.

Основной принцип тестирования Yup схем

Тестирование схем строится вокруг двух ключевых сценариев:

  • корректные данные проходят валидацию;
  • некорректные данные вызывают ошибки.

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

test('валидные данные проходят проверку', async () => {
  const schema = yup.object({
    name: yup.string().required(),
  });

  const data = { name: 'John' };

  const result = await schema.validate(data);

  expect(result).toEqual(data);
});

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

test('пустое значение вызывает ошибку', async () => {
  const schema = yup.object({
    name: yup.string().required(),
  });

  await expect(schema.validate({ name: '' }))
    .rejects
    .toThrow();
});

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

Строки

const schema = yup.string().min(3).max(10);

Проверка граничных значений:

test('строка короче минимальной длины', async () => {
  const schema = yup.string().min(3);

  await expect(schema.validate('ab'))
    .rejects
    .toThrow();
});
test('строка соответствует требованиям длины', async () => {
  const schema = yup.string().min(3);

  const result = await schema.validate('abcd');

  expect(result).toBe('abcd');
});

Числа

const schema = yup.number().positive().integer();

Тестирование отрицательных значений:

test('отрицательное число не проходит валидацию', async () => {
  const schema = yup.number().positive();

  await expect(schema.validate(-5))
    .rejects
    .toThrow();
});

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

const schema = yup.object({
  email: yup.string().email().required(),
});
test('отсутствие обязательного поля вызывает ошибку', async () => {
  const schema = yup.object({
    email: yup.string().email().required(),
  });

  await expect(schema.validate({}))
    .rejects
    .toThrow();
});

Особое внимание уделяется различию между undefined, null и пустой строкой, так как Yup обрабатывает их по-разному.

Тестирование объектных схем

Объектные схемы требуют проверки вложенной структуры:

const schema = yup.object({
  user: yup.object({
    name: yup.string().required(),
    age: yup.number().min(18),
  }),
});

Тест корректного объекта:

test('валидный вложенный объект', async () => {
  const data = {
    user: {
      name: 'Alex',
      age: 25,
    },
  };

  const result = await schema.validate(data);

  expect(result.user.name).toBe('Alex');
});

Тест частично некорректной структуры:

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

  await expect(schema.validate(data))
    .rejects
    .toThrow();
});

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

const schema = yup.object({
  tags: yup.array().of(yup.string().min(2)),
});

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

test('валидный массив строк', async () => {
  const data = {
    tags: ['js', 'node'],
  };

  const result = await schema.validate(data);

  expect(result.tags.length).toBe(2);
});

Ошибочные элементы массива:

test('элемент массива нарушает правило', async () => {
  const data = {
    tags: ['j', 'node'],
  };

  await expect(schema.validate(data))
    .rejects
    .toThrow();
});

Кастомные проверки через test()

Yup позволяет задавать собственные правила через .test().

const schema = yup.string().test(
  'no-spaces',
  'строка не должна содержать пробелы',
  value => !/\s/.test(value)
);

Тест кастомной логики:

test('строка с пробелами отклоняется', async () => {
  const schema = yup.string().test(
    'no-spaces',
    'error',
    value => !/\s/.test(value)
  );

  await expect(schema.validate('hello world'))
    .rejects
    .toThrow();
});

Тестирование трансформаций

Yup поддерживает преобразование значений через transform.

const schema = yup.number().transform((value, originalValue) => {
  return typeof originalValue === 'string'
    ? Number(originalValue)
    : value;
});

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

test('строка преобразуется в число', async () => {
  const schema = yup.number().transform((value, originalValue) => {
    return Number(originalValue);
  });

  const result = await schema.validate('42');

  expect(result).toBe(42);
});

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

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

const schema = yup.string().test(
  'is-unique',
  'значение уже используется',
  async (value) => {
    const exists = await fakeApiCheck(value);
    return !exists;
  }
);

Тестирование асинхронной логики требует мокирования:

const fakeApiCheck = jest.fn(async (value) => value === 'taken');

test('асинхронная проверка уникальности', async () => {
  const schema = yup.string().test(
    'is-unique',
    'error',
    async (value) => {
      const exists = await fakeApiCheck(value);
      return !exists;
    }
  );

  await expect(schema.validate('taken'))
    .rejects
    .toThrow();
});

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

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

const schema = yup.number().positive();

test.each([
  -1,
  -10,
  0,
])('некорректное значение %s', async (value) => {
  await expect(schema.validate(value))
    .rejects
    .toThrow();
});

Проверка строгого режима

Yup поддерживает strict режим, отключающий приведение типов.

const schema = yup.number().strict();

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

test('строковое число не приводится', async () => {
  const schema = yup.number().strict();

  await expect(schema.validate('123'))
    .rejects
    .toThrow();
});

Повторное использование схем и тестов

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

const emailSchema = yup.string().email().required();
const validCases = [
  'test@mail.com',
  'user@domain.org',
];

validCases.forEach(value => {
  test(`валидный email ${value}`, async () => {
    const result = await emailSchema.validate(value);
    expect(result).toBe(value);
  });
});

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

Тестирование не ограничивается фактом ошибки, важна и её структура:

test('сообщение ошибки совпадает', async () => {
  const schema = yup.string().required('поле обязательно');

  try {
    await schema.validate('');
  } catch (err) {
    expect(err.message).toBe('поле обязательно');
  }
});

Комплексные схемы и сценарии

Реальные схемы часто комбинируют множество правил:

const schema = yup.object({
  username: yup.string().min(3).required(),
  password: yup.string().min(8).required(),
  age: yup.number().min(18),
});

Тестирование комбинаций:

test('валидный объект проходит все проверки', async () => {
  const data = {
    username: 'user123',
    password: 'strongpass',
    age: 20,
  };

  const result = await schema.validate(data);

  expect(result.username).toBe('user123');
});

Отрицательные сценарии как основа стабильности

Большая часть ценности тестирования схем Yup формируется через негативные кейсы:

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

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