Пошук уроків, статей та іншого контенту
Ознайомитеся з підходами до тестування NestJS-застосунків, Jest і структурою тестових файлів.
Тестування допомагає перевірити, що застосунок працює правильно, а зміни в коді не ламають уже реалізовану функціональність.
У NestJS зазвичай використовують Jest — тестовий фреймворк, який уміє:
запускати тести;
перевіряти очікувані результати;
створювати моки залежностей;
показувати статистику покриття коду;
працювати з асинхронним кодом.
У новому проєкті NestJS Jest зазвичай уже налаштований.
Модульний тест перевіряє невелику частину програми окремо:
сервіс;
контролер;
guard;
pipe;
інший провайдер.
Такий тест не повинен залежати від бази даних, мережі або зовнішніх сервісів. Замість реальних залежностей використовують моки.
Наприклад, можна перевірити, чи правильно сервіс формує результат, не підключаючись до бази даних.
Інтеграційний тест перевіряє взаємодію кількох частин застосунку. Наприклад:
сервіс разом із репозиторієм;
контролер разом із сервісом;
кілька провайдерів одного модуля.
Такі тести реалістичніші, але зазвичай складніші та повільніші за модульні.
E2E-тест, або end-to-end test, перевіряє застосунок майже так, як його використовує клієнт:
запускається NestJS-модуль;
надсилається HTTP-запит;
перевіряється статус відповіді та її дані.
E2E-тести корисні для перевірки повного маршруту від HTTP-запиту до відповіді.
У NestJS тестові файли зазвичай мають суфікс .spec.ts:
src/
├── app.controller.ts
├── app.controller.spec.ts
├── app.service.ts
├── app.module.ts
└── users/
├── users.controller.ts
├── users.controller.spec.ts
├── users.service.ts
└── users.service.spec.tsФайл app.service.spec.ts містить тести для AppService, а users.controller.spec.ts — тести для UsersController.
Назва .spec.ts важлива, тому що Jest налаштований шукати файли з таким суфіксом.
Зазвичай тест має таку структуру:
describe('Назва компонента', () => {
beforeEach(() => {
// Підготовка перед кожним тестом
});
it('опис очікуваної поведінки', () => {
// Перевірка
});
});describe об’єднує тести однієї функції або компонента;
it описує один конкретний сценарій;
beforeEach виконується перед кожним тестом;
expect перевіряє результат.
У типовому проєкті NestJS доступні такі команди:
npm testЗапускає всі модульні тести.
npm test -- --watchЗапускає Jest у режимі спостереження. Тести повторно виконуються після зміни файлів.
npm run test:covЗапускає тести та показує покриття коду.
npm run test:e2eЗапускає E2E-тести, якщо вони налаштовані в проєкті.
Команди визначаються в package.json. Типовий фрагмент може мати такий вигляд:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json"
}
}Розглянемо простий сервіс:
// src/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}Тест для цього сервісу:
// src/app.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppService } from './app.service';
describe('AppService', () => {
let service: AppService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AppService],
}).compile();
service = module.get<AppService>(AppService);
});
it('має бути визначеним', () => {
expect(service).toBeDefined();
});
it('повертає привітання', () => {
expect(service.getHello()).toBe('Hello World!');
});
});Тут відбувається кілька дій:
Test.createTestingModule створює тестовий NestJS-модуль.
providers: [AppService] додає сервіс до цього модуля.
compile() створює модуль.
module.get(AppService) отримує екземпляр сервісу.
expect перевіряє результат роботи методу.
TestingModule імітує звичайний NestJS-модуль, тому залежності компонента обробляються через механізм dependency injection.
Jest має багато matchers — методів для перевірки результатів:
expect(value).toBe(5);
expect(value).toEqual({ name: 'Anna' });
expect(value).toBeTruthy();
expect(value).toBeDefined();
expect(array).toContain('admin');
expect(() => service.run()).toThrow();Найчастіше використовуються:
toBe — точне порівняння примітивних значень;
toEqual — порівняння об’єктів і масивів;
toBeDefined — значення не є undefined;
toBeTruthy — значення є істинним;
toContain — масив або рядок містить задане значення;
toThrow — функція викидає помилку.
Сервіс часто залежить від іншого сервісу або репозиторію. У модульному тесті зовнішню залежність замінюють моком.
Розглянемо два сервіси:
// src/users/users.repository.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersRepository {
findNameById(id: number): string | undefined {
if (id === 1) {
return 'Anna';
}
return undefined;
}
}// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
getName(id: number): string {
const name = this.usersRepository.findNameById(id);
if (!name) {
throw new NotFoundException('Користувача не знайдено');
}
return name;
}
}У тесті не потрібно використовувати справжній UsersRepository. Ми можемо створити його мок:
// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
let repository: {
findNameById: jest.Mock;
};
beforeEach(async () => {
repository = {
findNameById: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: repository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('повертає ім’я користувача', () => {
repository.findNameById.mockReturnValue('Anna');
const result = service.getName(1);
expect(result).toBe('Anna');
expect(repository.findNameById).toHaveBeenCalledWith(1);
});
it('викидає помилку, якщо користувача не знайдено', () => {
repository.findNameById.mockReturnValue(undefined);
expect(() => service.getName(10)).toThrow(NotFoundException);
});
});У цьому прикладі:
jest.fn() створює функцію-мок;
mockReturnValue задає значення, яке поверне мок;
toHaveBeenCalledWith(1) перевіряє, з яким аргументом викликали метод;
useValue передає тестовий об’єкт замість справжнього провайдера.
Так тест залежить лише від UsersService. Поведінка репозиторію задається безпосередньо в тесті.
Контролер зазвичай викликає метод сервісу та повертає його результат. Для його тестування сервіс також можна замінити моком.
// src/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id/name')
getName(@Param('id') id: string): string {
return this.usersService.getName(Number(id));
}
}Тест контролера:
// src/users/users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: {
getName: jest.Mock;
};
beforeEach(async () => {
service = {
getName: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: service,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('передає ідентифікатор сервісу та повертає ім’я', () => {
service.getName.mockReturnValue('Anna');
const result = controller.getName('1');
expect(service.getName).toHaveBeenCalledWith(1);
expect(result).toBe('Anna');
});
});Контролер отримує параметри маршруту як рядки, тому в цьому прикладі '1' перетворюється на число 1.
Модульний тест контролера не перевіряє HTTP-сервер. Він перевіряє логіку самого методу контролера.
Якщо метод повертає Promise, тест також має бути асинхронним.
it('отримує користувача асинхронно', async () => {
repository.findById.mockResolvedValue({
id: 1,
name: 'Anna',
});
const user = await service.findById(1);
expect(user).toEqual({
id: 1,
name: 'Anna',
});
});Для перевірки помилки можна використати rejects:
it('повертає помилку для невідомого користувача', async () => {
repository.findById.mockResolvedValue(undefined);
await expect(service.findById(10)).rejects.toThrow(NotFoundException);
});Важливо:
перед expect потрібно використовувати await;
для успішного результату використовують resolves;
для помилок у промісах використовують rejects.
E2E-тести часто зберігають в окремій директорії:
test/
├── app.e2e-spec.ts
└── jest-e2e.jsonПриклад E2E-тесту для стандартного контролера NestJS:
// test/app.e2e-spec.ts
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});Цей тест:
створює застосунок на основі AppModule;
запускає NestJS HTTP-сервер;
надсилає GET-запит до /;
перевіряє статус 200;
перевіряє текст відповіді;
закриває застосунок після тесту.
Для такого тесту в проєкті має бути доступний пакет supertest. У типовому NestJS-проєкті він зазвичай уже встановлений для E2E-тестів.
Іноді перед тестами потрібно створити спільні дані або підготувати мок.
Jest надає кілька функцій:
beforeEach — перед кожним тестом;
afterEach — після кожного тесту;
beforeAll — один раз перед усіма тестами;
afterAll — один раз після всіх тестів.
Наприклад:
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
service = new CalculatorService();
});
afterEach(() => {
jest.clearAllMocks();
});
it('додає числа', () => {
expect(service.add(2, 3)).toBe(5);
});
});jest.clearAllMocks() очищає інформацію про виклики моків. Це допомагає зробити тести незалежними один від одного.
Хороший тест зазвичай має три частини:
Arrange — підготовка даних і залежностей.
Act — виклик коду, який тестується.
Assert — перевірка результату.
Наприклад:
it('повертає ім’я користувача', () => {
// Arrange
repository.findNameById.mockReturnValue('Anna');
// Act
const result = service.getName(1);
// Assert
expect(result).toBe('Anna');
});Тест має перевіряти поведінку, а не внутрішню реалізацію. Якщо змінюється спосіб виконання операції, але результат залишається правильним, тест не повинен ламатися без причини.
Якщо сервіс або його залежність не додані до providers, NestJS не зможе створити тестовий модуль.
Неправильно:
await Test.createTestingModule({
providers: [],
}).compile();Якщо тестується UsersService, його потрібно додати:
await Test.createTestingModule({
providers: [UsersService],
}).compile();А його залежності потрібно додати реальними провайдерами або моками.
Модульний тест має бути швидким і незалежним. Підключення до справжньої бази даних робить його повільнішим і складнішим.
Для залежностей використовуйте моки через useValue або jest.fn().
await в асинхронному тестіНеправильно:
it('перевіряє результат', () => {
expect(service.findUser()).resolves.toEqual({ id: 1 });
});Правильно:
it('перевіряє результат', async () => {
await expect(service.findUser()).resolves.toEqual({ id: 1 });
});Без await Jest може завершити тест раніше, ніж буде отримано результат промісу.
Кожен тест повинен самостійно готувати свої дані. Не варто покладатися на те, що попередній тест змінив спільний стан.
Для скидання стану використовуйте beforeEach, afterEach та очищення моків.
Один тест краще присвятити одному сценарію. Наприклад:
успішне отримання даних;
відсутність даних;
помилка залежності.
Так помилки легше знаходити й виправляти.
У NestJS для тестування зазвичай використовують Jest.
Модульні тести перевіряють окремі класи та провайдери.
Інтеграційні тести перевіряють взаємодію кількох частин застосунку.
E2E-тести перевіряють повний HTTP-сценарій.
Тестові файли зазвичай мають суфікс .spec.ts.
Test.createTestingModule створює тестовий NestJS-модуль.
Залежності можна замінювати моками через useValue і jest.fn().
Для асинхронних операцій потрібно використовувати async і await.
Кожен тест має бути незалежним і перевіряти одну конкретну поведінку.