Пошук уроків, статей та іншого контенту
Протестуєте трансформацію та валідацію вхідних даних за допомогою custom і вбудованих pipes.
Pipe отримує значення та метадані аргументу, після чого може:
повернути трансформоване значення;
повернути значення без змін;
кинути виняток, якщо дані невалідні.
Тому під час unit-тестування pipe потрібно перевірити:
коректну трансформацію валідного значення;
відхилення невалідного значення;
тип і статус винятку;
роботу з ArgumentMetadata, якщо pipe залежить від метаданих;
побічні результати вбудованих pipes, наприклад видалення зайвих властивостей у ValidationPipe.
Метод transform() можна викликати напряму. Для unit-тесту pipe не потрібно створювати повний HTTP-запит.
Розглянемо pipe, який перетворює рядок на додатне ціле число.
// parse-positive-int.pipe.ts
import {
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParsePositiveIntPipe
implements PipeTransform<string, number>
{
transform(value: string): number {
const parsedValue = Number(value);
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
throw new BadRequestException(
'Значення має бути додатним цілим числом',
);
}
return parsedValue;
}
}У тесті для успішного сценарію перевіряємо, що pipe повертає число, а не початковий рядок:
// parse-positive-int.pipe.spec.ts
import {
BadRequestException,
ArgumentMetadata,
} from '@nestjs/common';
import { ParsePositiveIntPipe } from './parse-positive-int.pipe';
describe('ParsePositiveIntPipe', () => {
const pipe = new ParsePositiveIntPipe();
const metadata: ArgumentMetadata = {
type: 'param',
data: 'id',
metatype: Number,
};
it('перетворює рядок на додатне ціле число', () => {
const result = pipe.transform('42', metadata);
expect(result).toBe(42);
expect(typeof result).toBe('number');
});
it.each(['0', '-5', '1.5', 'abc', ''])(
'кидає BadRequestException для значення "%s"',
(value) => {
expect(() => pipe.transform(value, metadata)).toThrow(
BadRequestException,
);
},
);
});it.each() дає змогу перевірити однакову поведінку для кількох значень без дублювання тестів.
Окремо можна перевірити статус і повідомлення винятку:
it('повертає статус 400 для невалідного значення', () => {
try {
pipe.transform('not-a-number', metadata);
fail('Очікувався виняток');
} catch (error) {
expect(error).toBeInstanceOf(BadRequestException);
const exception = error as BadRequestException;
expect(exception.getStatus()).toBe(400);
expect(exception.getResponse()).toMatchObject({
message: 'Значення має бути додатним цілим числом',
statusCode: 400,
});
}
});Перевіряти конкретне повідомлення варто тоді, коли воно є частиною контракту pipe. Якщо повідомлення може змінюватися без впливу на поведінку, достатньо перевірити тип винятку та HTTP-статус.
ParseIntPipeВбудований ParseIntPipe також можна створити напряму та перевірити його метод transform().
import {
ArgumentMetadata,
BadRequestException,
ParseIntPipe,
} from '@nestjs/common';
describe('ParseIntPipe', () => {
const pipe = new ParseIntPipe();
const metadata: ArgumentMetadata = {
type: 'param',
data: 'userId',
metatype: Number,
};
it('перетворює коректний рядок на число', () => {
expect(pipe.transform('17', metadata)).toBe(17);
});
it('кидає BadRequestException для некоректного числа', () => {
expect(() => pipe.transform('17abc', metadata)).toThrow(
BadRequestException,
);
});
it('кидає BadRequestException для порожнього значення', () => {
expect(() => pipe.transform('', metadata)).toThrow(
BadRequestException,
);
});
});Метадані не завжди впливають на результат конкретного pipe, але їх варто передавати в тесті. Так тест більше відповідає реальному виклику pipe фреймворком.
ValidationPipeValidationPipe зазвичай працює разом із DTO та декораторами з class-validator.
Розглянемо DTO для створення користувача:
// create-user.dto.ts
import { Type } from 'class-transformer';
import { IsInt, IsString, Min, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name!: string;
@Type(() => Number)
@IsInt()
@Min(18)
age!: number;
}У цьому прикладі:
name має бути рядком щонайменше з двох символів;
age має бути цілим числом не менше 18;
@Type(() => Number) перетворює рядкове значення віку на число;
ValidationPipe виконуватиме трансформацію та валідацію.
Тест для валідних даних:
// validation.pipe.spec.ts
import {
ArgumentMetadata,
BadRequestException,
ValidationPipe,
} from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
describe('ValidationPipe', () => {
const metadata: ArgumentMetadata = {
type: 'body',
metatype: CreateUserDto,
data: '',
};
it('трансформує та повертає екземпляр DTO', async () => {
const pipe = new ValidationPipe({
transform: true,
whitelist: true,
});
const result = await pipe.transform(
{
name: 'Olena',
age: '21',
extra: 'буде видалено',
},
metadata,
);
expect(result).toBeInstanceOf(CreateUserDto);
expect(result.name).toBe('Olena');
expect(result.age).toBe(21);
expect(typeof result.age).toBe('number');
expect(result.extra).toBeUndefined();
});
it('відхиляє невалідні дані', async () => {
const pipe = new ValidationPipe({
transform: true,
whitelist: true,
});
await expect(
pipe.transform(
{
name: 'O',
age: 'abc',
},
metadata,
),
).rejects.toBeInstanceOf(BadRequestException);
});
});Оскільки transform() може повертати Promise, для тестів потрібно використовувати async і await.
Якщо потрібно перевірити, які саме поля не пройшли валідацію, можна отримати тіло винятку:
it('повертає помилки для кожного невалідного поля', async () => {
const pipe = new ValidationPipe({
transform: true,
whitelist: true,
});
let error: unknown;
try {
await pipe.transform(
{
name: 'A',
age: 'abc',
},
metadata,
);
} catch (caughtError) {
error = caughtError;
}
expect(error).toBeInstanceOf(BadRequestException);
const exception = error as BadRequestException;
const response = exception.getResponse() as {
message: string[];
statusCode: number;
};
expect(exception.getStatus()).toBe(400);
expect(response.statusCode).toBe(400);
expect(response.message).toEqual(
expect.arrayContaining([
expect.stringContaining('name'),
expect.stringContaining('age'),
]),
);
});Точний текст повідомлень може залежати від версій class-validator та налаштувань DTO. Тому часто краще перевіряти наявність помилок для потрібних полів, а не повний масив рядків.
whitelistОпція whitelist: true видаляє властивості, для яких у DTO немає validation-декораторів.
it('видаляє властивості, яких немає в DTO', async () => {
const pipe = new ValidationPipe({
transform: true,
whitelist: true,
});
const result = await pipe.transform(
{
name: 'Olena',
age: '25',
role: 'admin',
},
metadata,
);
expect(result).toEqual({
name: 'Olena',
age: 25,
});
});Якщо замість видалення потрібно відхиляти зайві властивості, використовуйте forbidNonWhitelisted: true:
it('відхиляє зайві властивості', async () => {
const pipe = new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
});
await expect(
pipe.transform(
{
name: 'Olena',
age: '25',
role: 'admin',
},
metadata,
),
).rejects.toBeInstanceOf(BadRequestException);
});Ці тести перевіряють саме конфігурацію pipe, а не лише правила DTO.
Якщо custom pipe залежить від сервісу, його потрібно створити через TestingModule, передавши mock-залежність.
import { Test } from '@nestjs/testing';
describe('UserExistsPipe', () => {
it('може отримати mock-залежність через TestingModule', async () => {
const usersService = {
exists: jest.fn().mockResolvedValue(true),
};
const moduleRef = await Test.createTestingModule({
providers: [
UserExistsPipe,
{
provide: UsersService,
useValue: usersService,
},
],
}).compile();
const pipe = moduleRef.get(UserExistsPipe);
const result = await pipe.transform('user-1', {
type: 'param',
data: 'userId',
metatype: String,
});
expect(result).toBe('user-1');
expect(usersService.exists).toHaveBeenCalledWith('user-1');
});
});Для простих stateless pipes, які не мають залежностей, new Pipe() зазвичай достатньо. TestingModule потрібен тоді, коли важливо перевірити інжекцію залежностей або поведінку провайдерів.
Для unit-тесту pipe не потрібно запускати застосунок або створювати контролер. Викликайте transform() напряму. Повний HTTP-тест потрібен лише для перевірки взаємодії маршруту, контролера та глобальних pipes.
Якщо pipe повертає проміс, цей тест некоректний:
expect(pipe.transform(value, metadata)).toThrow();Потрібно дочекатися промісу:
await expect(
pipe.transform(value, metadata),
).rejects.toBeInstanceOf(BadRequestException);Валідація не завжди автоматично перетворює рядок на число. Для цього в тестованому ValidationPipe потрібно ввімкнути transform: true, а DTO має містити відповідні типи або @Type(() => Number).
metatypeValidationPipe використовує metadata.metatype, щоб зрозуміти, який DTO потрібно перевірити. Якщо передати звичайний Object замість CreateUserDto, правила цього DTO не застосуються.
Тест має перевіряти не тільки повернене значення, а й негативні сценарії:
некоректний формат;
порожнє значення;
значення за межами допустимого діапазону;
зайві властивості;
правильний тип винятку та статус 400.
Custom pipe тестують прямим викликом transform().
Для валідних даних перевіряйте значення та його тип.
Для невалідних даних перевіряйте BadRequestException і статус 400.
ParseIntPipe можна тестувати так само, як custom pipe.
ValidationPipe потрібно тестувати з правильним ArgumentMetadata, DTO та налаштуваннями transform і whitelist.
Асинхронні виклики перевіряйте через await і .rejects.
Pipes із залежностями створюйте через TestingModule і mock-об’єкти.