Интеграционное тестирование с использованием
class-validator концентрируется на проверке полного контура
обработки входных данных: от HTTP-запроса до выполнения бизнес-логики,
включая трансформацию DTO и применение правил валидации. Основная цель —
подтвердить, что система корректно отклоняет невалидные данные и
пропускает корректные на всех уровнях пайплайна.
В типичных архитектурах на базе Express или NestJS валидация
выполняется на уровне DTO-классов с декораторами
class-validator, а запуск происходит через middleware или
pipe (например, ValidationPipe в NestJS). Интеграционные
тесты проверяют именно это сочетание компонентов, а не отдельные
функции.
Библиотека class-validator работает поверх объектов
классов, применяя декораторы к свойствам:
@IsString()@IsInt()@Length()@IsEmail()@ValidateNested()Пример DTO:
import { IsString, IsEmail, Length } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@Length(3, 20)
password: string;
}
На уровне интеграции важно не только наличие этих правил, но и корректное поведение всей цепочки:
class-validatorИнтеграционные тесты обычно строятся на полном приложении с поднятым сервером.
В NestJS типичная конфигурация:
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.init();
});
Ключевым элементом является ValidationPipe, который
запускает class-validator на входных данных. Без него
декораторы не участвуют в обработке запроса.
Основная ценность интеграционных тестов проявляется в проверке отклонения некорректных данных.
Пример теста на отсутствие обязательного поля:
it('отклоняет запрос без email', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
password: 'strongpass123',
});
expect(response.status).toBe(400);
expect(response.body.message).toEqual(
expect.arrayContaining(['email should not be empty']),
);
});
При этом важна не только проверка статуса, но и структура ошибки, так
как она формируется на основе результатов
class-validator.
Интеграционные тесты часто охватывают комбинации декораторов:
it('отклоняет слишком короткий пароль', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'test@mail.com',
password: '12',
});
expect(response.status).toBe(400);
expect(response.body.message).toEqual(
expect.arrayContaining([
'password must be longer than or equal to 3 characters',
]),
);
});
Такие тесты фиксируют не только факт валидации, но и устойчивость бизнес-ограничений.
Одной из ключевых возможностей ValidationPipe является
фильтрация лишних данных:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
);
Интеграционный тест:
it('отклоняет запрос с лишними полями', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'test@mail.com',
password: 'strongpass',
role: 'admin',
});
expect(response.status).toBe(400);
expect(response.body.message).toContain('property role should not exist');
});
Здесь проверяется согласованность DTO и входного контракта API.
class-validator часто используется вместе с
class-transformer для сложных объектов:
import { ValidateNested, IsString } from 'class-validator';
import { Type } from 'class-transformer';
class ProfileDto {
@IsString()
firstName: string;
}
export class CreateUserDto {
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
Интеграционный тест:
it('валидирует вложенные DTO', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'test@mail.com',
password: 'strongpass',
profile: {
firstName: 123,
},
});
expect(response.status).toBe(400);
expect(response.body.message).toEqual(
expect.arrayContaining(['firstName must be a string']),
);
});
Важный аспект: без @Type() вложенная валидация не будет
выполнена корректно, что делает такие тесты критичными.
При включённом transform: true входные данные приводятся
к типам DTO.
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
Интеграционный тест может проверять не только валидацию, но и типизацию:
it('преобразует строковые числа в number', async () => {
const response = await request(app.getHttpServer())
.post('/orders')
.send({
amount: '100',
});
expect(response.status).toBe(201);
});
Внутри контроллера amount уже интерпретируется как
число, если DTO описан корректно.
Кастомные валидаторы через @ValidatorConstraint также
должны покрываться интеграционно, поскольку они часто зависят от
сервисов или базы данных.
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint {
constructor(private userService: UserService) {}
async validate(email: string) {
const user = await this.userService.findByEmail(email);
return !user;
}
}
Интеграционный тест:
it('отклоняет дублирующий email', async () => {
await request(app.getHttpServer())
.post('/users')
.send({
email: 'exists@mail.com',
password: 'strongpass',
});
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'exists@mail.com',
password: 'anotherpass',
});
expect(response.status).toBe(400);
});
Такие сценарии фиксируют корректную работу асинхронной валидации и взаимодействие с внешними слоями.
class-validator возвращает массив ошибок, который часто
преобразуется в единый формат ответа API.
Типичная структура:
{
"statusCode": 400,
"message": [
"email must be an email",
"password must be longer than or equal to 3 characters"
],
"error": "Bad Request"
}
Интеграционные тесты фиксируют стабильность этого контракта:
it('формирует корректный формат ошибок', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'invalid',
password: '1',
});
expect(response.body).toHaveProperty('statusCode', 400);
expect(Array.isArray(response.body.message)).toBe(true);
});
При интеграционных тестах с базой данных важно учитывать влияние валидных запросов на состояние системы. Часто используется транзакционный откат или очистка таблиц.
Пример с очисткой:
afterEach(async () => {
await userRepository.clear();
});
Это позволяет изолировать сценарии, связанные с валидацией уникальности, длины и формата данных.
Финальный тип интеграционного теста объединяет валидацию и бизнес-логику:
it('создаёт пользователя при валидных данных', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'new@mail.com',
password: 'strongpass123',
});
expect(response.status).toBe(201);
expect(response.body.email).toBe('new@mail.com');
});
Здесь проверяется, что валидатор пропускает данные дальше по цепочке, а система корректно выполняет создание сущности.
Интеграционные тесты часто включают проверку нестандартных входов:
null вместо строкиnumber вместо
string)Пример:
it('отклоняет null в обязательном поле', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: null,
password: 'strongpass',
});
expect(response.status).toBe(400);
});
Такие сценарии выявляют расхождения между ожиданиями DTO и реальными HTTP-пейлоадами.
Интеграционные тесты с class-validator фактически
закрепляют контракт между клиентом и сервером. Любое изменение
декораторов влияет на:
Поэтому тесты выступают точкой фиксации этих ограничений и предотвращают регрессии при изменении DTO или глобальных настроек пайпов.