Юнит-тестирование валидаторов в 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");
});
В конфигурации 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"
});
Использование таких фабрик снижает дублирование и повышает устойчивость тестового набора к изменениям схем.