Тестирование граничных случаев

Граничные случаи валидации схем в Ajv проявляются в ситуациях, где данные формально соответствуют типу, но выходят за пределы ожидаемых значений, структуры или поведения JavaScript-движка. Такие сценарии критичны для надёжности API, поскольку именно на них чаще всего возникают скрытые ошибки и расхождения между спецификацией и реальной обработкой данных.

JavaScript использует формат IEEE 754 для чисел с плавающей точкой, что накладывает ограничения на точность и диапазон.

В контексте Ajv это приводит к нескольким типичным граничным случаям:

  • превышение Number.MAX_SAFE_INTEGER
  • работа с Number.MIN_SAFE_INTEGER
  • потеря точности при больших значениях
  • некорректная интерпретация дробей
const schema = {
  type: "object",
  properties: {
    id: { type: "integer" }
  },
  required: ["id"]
};

Значения выше безопасного диапазона могут проходить проверку как числа, но терять точность ещё до валидации:

9007199254740993 === 9007199254740992 // true

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

const schema = {
  type: "object",
  properties: {
    id: {
      type: "string",
      pattern: "^[0-9]+$"
    }
  }
};

Пограничные состояния строк

Строки в JSON-валидации часто содержат скрытые граничные случаи:

  • пустая строка
  • строка из пробелов
  • Unicode-символы за пределами BMP
  • нормализация Unicode
  • комбинации управляющих символов
const schema = {
  type: "object",
  properties: {
    username: {
      type: "string",
      minLength: 3,
      maxLength: 20
    }
  }
};

Пустая строка ("") формально валидна как строка, но нарушает бизнес-ограничения. Ajv не интерпретирует смысл данных, поэтому проверка должна явно учитывать такие случаи через minLength.

Особую сложность создают Unicode-символы:

  • эмодзи занимают два UTF-16 блока
  • визуальная длина не совпадает с length
  • комбинируемые символы могут увеличивать реальный размер

null, undefined и отсутствие поля

Различие между отсутствием поля и значением null является частым источником ошибок.

const schema = {
  type: "object",
  properties: {
    title: { type: "string" }
  },
  required: ["title"]
};

Граничные случаи:

  • поле отсутствует полностью
  • поле равно null
  • поле равно undefined (в JavaScript до сериализации)

Ajv различает:

  • отсутствие ключа (ошибка при required)
  • null (ошибка типа, если не разрешён null)

Для поддержки null используется расширение:

const schema = {
  type: "object",
  properties: {
    title: {
      type: ["string", "null"]
    }
  }
};

Массивы и крайние состояния структуры

Граничные случаи массивов включают:

  • пустой массив
  • массив с одним элементом
  • разреженные массивы
  • чрезмерно большие массивы
const schema = {
  type: "array",
  items: { type: "number" },
  minItems: 1,
  maxItems: 5
};

Разреженные массивы ([1, , 3]) не всегда корректно сериализуются в JSON, но при обработке в JavaScript могут появляться. Ajv валидирует уже десериализованные данные, где такие элементы могут отсутствовать как undefined, что приводит к неожиданным результатам.

Сложные схемы: allOf, oneOf, anyOf

Комбинаторные конструкции часто создают неоднозначные граничные ситуации.

allOf

const schema = {
  allOf: [
    { type: "object", properties: { a: { type: "number" } } },
    { type: "object", properties: { b: { type: "number" } } }
  ]
};

Граничный случай возникает при пересечении ограничений, которые конфликтуют между собой, например разные type для одного поля.

oneOf

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

oneOf: [
  { type: "number" },
  { type: "integer" }
]

Любое целое число соответствует обеим схемам, что приводит к провалу валидации.

anyOf

При anyOf важен случай, когда данные формально проходят хотя бы одну схему, но логически не соответствуют ожиданиям системы.

Регулярные выражения и форматные ограничения

Ajv использует JavaScript RegExp, что создаёт следующие граничные ситуации:

  • catastrophic backtracking
  • различие между движками regex
  • обработка Unicode флагов
  • пустые совпадения
const schema = {
  type: "string",
  pattern: "^(a+)+$"
};

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

Крайние случаи дополнительных свойств

const schema = {
  type: "object",
  additionalProperties: false
};

Граничные ситуации:

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

В JavaScript объект может содержать свойства из цепочки прототипов, что иногда приводит к ложным срабатываниям при неправильной настройке.

Коэрция типов и неявные преобразования

При включённой опции coerceTypes возникают крайние случаи:

  • строка "0" → число 0
  • пустая строка → 0 или NaN
  • boolean-подобные строки
const ajv = new Ajv({ coerceTypes: true });

Пограничное поведение:

  • " " (пробел) превращается в 0
  • "true" не всегда интерпретируется как boolean
  • "1e2" становится числом с плавающей точкой

Значения по умолчанию

При использовании useDefaults возникают ситуации, где:

  • значение подставляется в отсутствующее поле
  • вложенные объекты получают частично заполненные структуры
  • массивы инициализируются неожиданным образом
const schema = {
  type: "object",
  properties: {
    count: {
      type: "number",
      default: 10
    }
  }
};

Граничный случай — различие между undefined и отсутствием ключа, особенно при частичном обновлении данных.

Глубоко вложенные структуры

Рекурсивные схемы создают особые крайние ситуации:

  • переполнение стека при слишком глубокой вложенности
  • деградация производительности
  • циклические ссылки
const node = {
  type: "object",
  properties: {
    child: { $ref: "#" }
  }
};

Циклические структуры требуют аккуратной настройки, иначе возможны бесконечные обходы при рекурсивной проверке.

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

При использовании асинхронных ключей (async validate) возникают граничные состояния:

  • частичное завершение проверки
  • гонки между результатами промисов
  • неконсистентное состояние кеша схем

Особенно заметно при массовой валидации больших массивов объектов.

Строгий режим и неизвестные ключи

При включённом strict режиме Ajv начинает трактовать неизвестные ключи как ошибку схемы.

Граничные случаи:

  • разные версии схем
  • динамическая генерация объектов
  • расширяемые API

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

Поведение при частичных ошибках

Ajv может возвращать несколько ошибок одновременно, но порядок их появления не гарантирован.

Граничные случаи:

  • зависимость ошибок друг от друга
  • перекрывающиеся ограничения
  • раннее завершение проверки (allErrors: false)

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