Пошук уроків, статей та іншого контенту
Перевірите перехоплення запитів, модифікацію відповідей і обробку контексту в interceptors.
Interceptor отримує два об’єкти:
ExecutionContext — контекст поточного запиту;
CallHandler — об’єкт, через який викликається наступний обробник.
Метод next.handle() повертає Observable. Тому під час тестування потрібно не лише викликати intercept(), а й підписатися на результат або перетворити його на Promise.
У цьому уроці протестуємо interceptor, який:
отримує HTTP-контекст;
читає метод і URL запиту;
викликає наступний обробник;
додає до відповіді об’єкт meta.
Створимо EnvelopeInterceptor:
// envelope.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class EnvelopeInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const request = context
.switchToHttp()
.getRequest<{ method: string; url: string }>();
return next.handle().pipe(
map((data: unknown) => ({
data,
meta: {
method: request.method,
url: request.url,
},
})),
);
}
}Interceptor використовує switchToHttp(), щоб отримати HTTP-контекст. Після цього з об’єкта запиту читаються method і url.
Виклик next.handle() повертає початкову відповідь. Оператор map() змінює її формат:
{
data: початковаВідповідь,
meta: {
method: 'GET',
url: '/users/42'
}
}ExecutionContextУ unit-тесті не потрібно створювати справжній HTTP-запит. Достатньо змокувати ті методи контексту, які використовує interceptor.
У нашому випадку потрібні:
context.switchToHttp();
результат роботи switchToHttp().getRequest().
Структура мока має відповідати ланцюжку викликів interceptor:
const context = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
method: 'GET',
url: '/users/42',
}),
}),
} as unknown as ExecutionContext;ExecutionContext містить багато методів, але для конкретного тесту достатньо реалізувати лише ті, які реально викликаються.
CallHandlerCallHandler має метод handle(), який повертає Observable.
Для успішного сценарію можна використати of() з RxJS:
const next: CallHandler = {
handle: jest.fn().mockReturnValue(
of({
id: 42,
name: 'Ada',
}),
),
};Тепер next.handle() поверне Observable з об’єктом користувача.
Файл тесту:
// envelope.interceptor.spec.ts
import { CallHandler, ExecutionContext } from '@nestjs/common';
import { lastValueFrom, of } from 'rxjs';
import { EnvelopeInterceptor } from './envelope.interceptor';
describe('EnvelopeInterceptor', () => {
it('додає інформацію про запит до відповіді', async () => {
const interceptor = new EnvelopeInterceptor();
const context = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
method: 'GET',
url: '/users/42',
}),
}),
} as unknown as ExecutionContext;
const next: CallHandler = {
handle: jest.fn().mockReturnValue(
of({
id: 42,
name: 'Ada',
}),
),
};
const result = (await lastValueFrom(
interceptor.intercept(context, next),
)) as {
data: {
id: number;
name: string;
};
meta: {
method: string;
url: string;
};
};
expect(result).toEqual({
data: {
id: 42,
name: 'Ada',
},
meta: {
method: 'GET',
url: '/users/42',
},
});
});
});lastValueFrom() очікує завершення Observable і повертає його останнє значення як Promise. Це дає змогу використовувати звичайні асинхронні assertions Jest.
Окрім результату, потрібно перевірити, що interceptor справді використав HTTP-контекст і прочитав потрібні дані запиту.
// envelope.interceptor.spec.ts
import { CallHandler, ExecutionContext } from '@nestjs/common';
import { lastValueFrom, of } from 'rxjs';
import { EnvelopeInterceptor } from './envelope.interceptor';
describe('EnvelopeInterceptor', () => {
it('отримує HTTP-контекст і запит', async () => {
const interceptor = new EnvelopeInterceptor();
const getRequest = jest.fn().mockReturnValue({
method: 'POST',
url: '/users',
});
const switchToHttp = jest.fn().mockReturnValue({
getRequest,
});
const context = {
switchToHttp,
} as unknown as ExecutionContext;
const next: CallHandler = {
handle: jest.fn().mockReturnValue(of({ id: 1 })),
};
await lastValueFrom(interceptor.intercept(context, next));
expect(switchToHttp).toHaveBeenCalledTimes(1);
expect(getRequest).toHaveBeenCalledTimes(1);
});
it('викликає наступний обробник один раз', async () => {
const interceptor = new EnvelopeInterceptor();
const context = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
method: 'GET',
url: '/health',
}),
}),
} as unknown as ExecutionContext;
const handle = jest.fn().mockReturnValue(of({ status: 'ok' }));
const next: CallHandler = {
handle,
};
await lastValueFrom(interceptor.intercept(context, next));
expect(handle).toHaveBeenCalledTimes(1);
});
});Такі перевірки захищають interceptor від помилок, коли:
використовується неправильний тип контексту;
getRequest() не викликається;
наступний обробник викликається кілька разів;
відповідь формується без урахування даних запиту.
Interceptor повинен коректно обробляти не лише об’єкти. Якщо контролер повертає масив, рядок або null, оператор map() у нашому прикладі просто помістить це значення у властивість data.
// envelope.interceptor.spec.ts
import { CallHandler, ExecutionContext } from '@nestjs/common';
import { lastValueFrom, of } from 'rxjs';
import { EnvelopeInterceptor } from './envelope.interceptor';
describe('EnvelopeInterceptor: типи відповідей', () => {
const createContext = (): ExecutionContext =>
({
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
method: 'GET',
url: '/values',
}),
}),
}) as unknown as ExecutionContext;
it('обгортає масив у data', async () => {
const interceptor = new EnvelopeInterceptor();
const next: CallHandler = {
handle: jest.fn().mockReturnValue(of([1, 2, 3])),
};
const result = (await lastValueFrom(
interceptor.intercept(createContext(), next),
)) as { data: number[] };
expect(result.data).toEqual([1, 2, 3]);
});
it('обгортає null у data', async () => {
const interceptor = new EnvelopeInterceptor();
const next: CallHandler = {
handle: jest.fn().mockReturnValue(of(null)),
};
const result = (await lastValueFrom(
interceptor.intercept(createContext(), next),
)) as { data: null };
expect(result.data).toBeNull();
});
});Такі тести особливо корисні для interceptor, який застосовується глобально. Різні контролери можуть повертати різні типи значень.
Якщо наступний обробник повертає Observable з помилкою, interceptor у поточній реалізації не перехоплює її і не змінює. Помилка має бути передана далі.
// envelope.interceptor.spec.ts
import { CallHandler, ExecutionContext } from '@nestjs/common';
import { lastValueFrom, throwError } from 'rxjs';
import { EnvelopeInterceptor } from './envelope.interceptor';
describe('EnvelopeInterceptor: помилки', () => {
it('передає помилку від наступного обробника', async () => {
const interceptor = new EnvelopeInterceptor();
const context = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
method: 'GET',
url: '/users',
}),
}),
} as unknown as ExecutionContext;
const next: CallHandler = {
handle: jest.fn().mockReturnValue(
throwError(() => new Error('Database unavailable')),
),
};
await expect(
lastValueFrom(interceptor.intercept(context, next)),
).rejects.toThrow('Database unavailable');
});
});Цей тест фіксує поточну поведінку interceptor: він змінює лише успішну відповідь і не приховує помилки.
TestingModuleЯкщо interceptor має залежності з Dependency Injection, його зручно створювати через TestingModule. Для interceptor без залежностей пряме створення через new є простішим.
Приклад із TestingModule:
// envelope.interceptor.spec.ts
import { Test } from '@nestjs/testing';
import { EnvelopeInterceptor } from './envelope.interceptor';
describe('EnvelopeInterceptor через TestingModule', () => {
it('створюється контейнером NestJS', async () => {
const moduleRef = await Test.createTestingModule({
providers: [EnvelopeInterceptor],
}).compile();
const interceptor = moduleRef.get(EnvelopeInterceptor);
expect(interceptor).toBeInstanceOf(EnvelopeInterceptor);
});
});TestingModule перевіряє, що interceptor правильно зареєстрований у контейнері NestJS. Водночас він не замінює перевірки логіки intercept() — для цього все одно потрібно змокувати ExecutionContext і CallHandler.
Для NestJS-проєкту з налаштованим Jest достатньо розмістити файли поруч:
src/
common/
interceptors/
envelope.interceptor.ts
envelope.interceptor.spec.tsЗапустити тести можна командою:
npm test -- envelope.interceptor.spec.tsЯкщо потрібно перевірити покриття:
npm run test:cov -- envelope.interceptor.spec.tsВиклик intercept() сам по собі не завжди запускає логіку Observable. Якщо не використати lastValueFrom, firstValueFrom або підписку, тест може не перевірити фактичний результат.
Неповна перевірка:
const result = interceptor.intercept(context, next);
expect(result).toBeDefined();Така перевірка підтверджує лише, що повернувся Observable.
Краще перевіряти значення:
const result = await lastValueFrom(
interceptor.intercept(context, next),
);
expect(result).toEqual(expectedResponse);handle() звичайне значенняCallHandler.handle() має повертати Observable. Не слід передавати туди безпосередньо об’єкт:
const next = {
handle: jest.fn().mockReturnValue({ id: 1 }),
};Правильний варіант:
const next = {
handle: jest.fn().mockReturnValue(of({ id: 1 })),
};Якщо interceptor викликає:
context.switchToHttp().getRequest()то мок повинен містити обидва методи:
const context = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue(request),
}),
};Мок лише getRequest() не працюватиме, оскільки цей метод належить не ExecutionContext, а результату switchToHttp().
next.handle()Перевірка виклику наступного обробника важлива, але вона не підтверджує правильність модифікації відповіді. Тест має перевіряти і взаємодію, і кінцеве значення:
expect(next.handle).toHaveBeenCalledTimes(1);
expect(result).toEqual(expectedResponse);Unit-тест interceptor не потребує запуску сервера або надсилання HTTP-запиту. ExecutionContext і CallHandler можна замінити компактними моками.
Інтеграційні та end-to-end тести потрібні для перевірки повного ланцюжка NestJS, але логіку самого interceptor швидше й точніше перевіряти ізольовано.
ExecutionContext у unit-тесті потрібно замінити мок-об’єктом із необхідними методами.
Для HTTP-interceptor зазвичай мокуються switchToHttp() і getRequest().
CallHandler.handle() повинен повертати Observable, наприклад через of().
Результат interceptor потрібно перевіряти після підписки, використовуючи lastValueFrom().
Окремо перевіряйте:
отримання даних із контексту;
виклик next.handle();
модифікацію успішної відповіді;
передавання помилок;
роботу з різними типами результатів.