Unit-тестирование схем Joi строится вокруг проверки соответствия входных данных контракту, заданному через валидатор. Основная цель тестов — гарантировать, что каждая схема корректно принимает валидные данные и отклоняет некорректные во всех ожидаемых сценариях, включая граничные случаи и нестандартные входы.
Любая схема в Joi представляет собой набор правил валидации, который можно протестировать как обычную чистую функцию: на вход подаётся объект данных, на выходе получается либо результат с преобразованными значениями, либо ошибка валидации.
Ключевые свойства, которые проверяются в unit-тестах:
Наиболее часто используется связка 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;
})
});
detailsПрактика структурирования тестов обычно следует логике самой схемы:
Такой подход позволяет поддерживать предсказуемое поведение схем Joi при изменениях и расширении логики валидации.