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

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

При работе с Ajv каждая JSON Schema компилируется в функцию валидации. Именно эта скомпилированная функция становится объектом тестирования. Ключевой момент заключается в том, что тестируется не библиотека, а конкретная схема и её поведение на наборах данных.

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

import Ajv from "ajv";

const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string" }
  },
  required: ["id", "name"],
  additionalProperties: false
};

const validate = ajv.compile(schema);

Тестирование строится вокруг трёх классов входных данных:

  • корректные данные
  • некорректные данные по типам
  • данные, нарушающие структуру схемы

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

Юнит-тест для положительного сценария фиксирует ожидаемое поведение схемы:

test("валидные данные проходят проверку", () => {
  const data = { id: 1, name: "Item" };

  const valid = validate(data);

  expect(valid).toBe(true);
  expect(validate.errors).toBeNull();
});

Отсутствие ошибок является важным признаком корректной настройки схемы. Даже если true возвращается, наличие ошибок может сигнализировать о побочных эффектах повторного использования валидатора.

Проверка ошибок валидации

Негативные сценарии требуют анализа структуры errors, которую формирует Ajv:

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

  const valid = validate(data);

  expect(valid).toBe(false);
  expect(validate.errors).toBeDefined();
  expect(validate.errors[0].keyword).toBe("required");
});

Поле keyword позволяет точно определить тип нарушения: required, type, additionalProperties и другие.

Проверка типов данных

Отдельный класс тестов связан с контролем типов:

test("неверный тип данных отклоняется", () => {
  const data = { id: "1", name: "Item" };

  const valid = validate(data);

  expect(valid).toBe(false);
  expect(validate.errors[0].keyword).toBe("type");
});

Такие тесты критичны для схем, используемых в API, где типизация определяет контракт взаимодействия.

Изоляция скомпилированных валидаторов

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

let validate;

beforeEach(() => {
  validate = ajv.compile(schema);
});

Такой подход гарантирует, что каждый тест работает с чистым состоянием.

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

Сложные схемы с вложенными объектами требуют проверки не только верхнего уровня, но и глубинных свойств:

const schema = {
  type: "object",
  properties: {
    user: {
      type: "object",
      properties: {
        profile: {
          type: "object",
          properties: {
            age: { type: "number", minimum: 18 }
          },
          required: ["age"]
        }
      },
      required: ["profile"]
    }
  }
};

Тестирование таких структур строится на проверке конкретных путей ошибок:

test("ошибка во вложенном объекте корректно фиксируется", () => {
  const data = {
    user: {
      profile: {
        age: 15
      }
    }
  };

  const valid = validate(data);

  expect(valid).toBe(false);
  expect(validate.errors[0].instancePath).toContain("age");
});

Поле instancePath указывает точное расположение нарушения.

Проверка пользовательских форматов и ключевых слов

Ajv позволяет добавлять собственные форматы и keywords, которые также требуют тестирования.

Пример регистрации формата:

ajv.addFormat("evenNumber", {
  type: "number",
  validate: (x) => x % 2 === 0
});

Тест:

const schema = {
  type: "number",
  format: "evenNumber"
};

const validate = ajv.compile(schema);

test("кастомный формат проверяет чётность", () => {
  expect(validate(4)).toBe(true);
  expect(validate(3)).toBe(false);
});

Тестирование массивов и ограничений длины

Схемы с массивами требуют проверки граничных условий:

const schema = {
  type: "array",
  minItems: 1,
  maxItems: 3,
  items: { type: "string" }
};
test("массив вне допустимой длины отклоняется", () => {
  const data = [];

  expect(validate(data)).toBe(false);
  expect(validate.errors[0].keyword).toBe("minItems");
});

Проверка поведения при multiple errors

В конфигурации Ajv возможно включение режима сбора всех ошибок:

const ajv = new Ajv({ allErrors: true });

Это меняет стратегию тестирования: проверяется не одна ошибка, а их набор.

test("собираются все ошибки", () => {
  const data = {};

  validate(data);

  expect(validate.errors.length).toBeGreaterThan(1);
});

Тестирование условий oneOf, anyOf, allOf

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

Пример oneOf:

const schema = {
  oneOf: [
    { type: "string" },
    { type: "number" }
  ]
};
test("oneOf принимает только одно соответствие", () => {
  expect(validate("text")).toBe(true);
  expect(validate(123)).toBe(true);
  expect(validate({})).toBe(false);
});

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

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

При использовании асинхронных keyword или внешних форматов тесты должны учитывать Promise:

const validate = ajv.compileAsync(schema);

test("асинхронная валидация", async () => {
  const valid = await validate(data);

  expect(valid).toBe(true);
});

Важно учитывать, что ошибки также возвращаются через rejected Promise.

Проверка стабильности схемы

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

test("валидатор детерминирован", () => {
  const data = { id: 1, name: "Item" };

  expect(validate(data)).toBe(true);
  expect(validate(data)).toBe(true);
});

Этот класс тестов выявляет проблемы побочных эффектов внутри кастомных правил.

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

Хотя Ajv фокусируется на структурной валидации, сообщения об ошибках могут быть важны для API:

test("ошибка содержит корректное поле", () => {
  validate({ id: "x" });

  expect(validate.errors[0].message).toContain("number");
});

Такие тесты полезны при использовании пользовательских errorMessage настроек.

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

При развитии API схемы могут изменяться. Тесты фиксируют обратную совместимость:

const oldSchema = ajv.compile(oldVersionSchema);
const newSchema = ajv.compile(newVersionSchema);

test("новая схема поддерживает старые данные", () => {
  const legacyData = { id: 1, name: "Item" };

  expect(newSchema(legacyData)).toBe(true);
});

Стратегии организации тестов

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

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

Фабрики данных упрощают поддержку тестов:

const createValidUser = () => ({
  id: 1,
  name: "User"
});

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