Пошук уроків, статей та іншого контенту
Створите тестовий модуль через TestingModule та налаштуєте залежності для ізольованих перевірок.
У NestJS залежності класу зазвичай створюються контейнером Dependency Injection. Наприклад, сервіс може отримувати репозиторій через конструктор:
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
}Під час звичайного запуску застосунку NestJS сам знаходить і створює UsersRepository. У юніт-тесті ми не хочемо підключати всю програму або справжню базу даних. Натомість створюємо невеликий тестовий контейнер із потрібними залежностями.
Для цього використовується TestingModule з пакета @nestjs/testing.
Тестовий модуль дає змогу:
зареєструвати клас, який перевіряємо;
додати його залежності;
замінити реальні залежності моками;
отримати екземпляр сервісу з контейнера NestJS;
перевірити поведінку класу в ізоляції.
Для створення тестового модуля використовується метод Test.createTestingModule():
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();Метод приймає конфігурацію, подібну до конфігурації звичайного NestJS-модуля:
providers — провайдери;
controllers — контролери;
imports — інші модулі;
exports — провайдери, доступні для імпорту.
Метод compile() асинхронно створює контейнер залежностей. Без виклику compile() тестовий модуль не буде готовим до використання.
Після компіляції залежність отримують через метод get():
const usersService = module.get(UsersService);Зазвичай модуль створюють у beforeEach, щоб кожен тест отримував новий ізольований контейнер.
Розглянемо сервіс, який використовує репозиторій користувачів.
import { Inject, Injectable } from '@nestjs/common';
export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
export interface UsersRepository {
findById(id: string): Promise<{ id: string; name: string } | null>;
}
@Injectable()
export class UsersService {
constructor(
@Inject(USERS_REPOSITORY)
private readonly usersRepository: UsersRepository,
) {}
async getUserName(id: string): Promise<string | null> {
const user = await this.usersRepository.findById(id);
return user?.name ?? null;
}
}Сервіс має одну залежність — провайдер із токеном USERS_REPOSITORY.
У реальному застосунку цим провайдером міг би бути репозиторій, який звертається до бази даних. Для юніт-тесту підключати базу не потрібно.
import { Test, TestingModule } from '@nestjs/testing';
import {
USERS_REPOSITORY,
UsersRepository,
UsersService,
} from './users.service';
describe('UsersService', () => {
let usersService: UsersService;
const usersRepositoryMock: UsersRepository = {
findById: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: USERS_REPOSITORY,
useValue: usersRepositoryMock,
},
],
}).compile();
usersService = module.get<UsersService>(UsersService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('повертає ім’я знайденого користувача', async () => {
usersRepositoryMock.findById = jest
.fn()
.mockResolvedValue({ id: '1', name: 'Олена' });
await expect(usersService.getUserName('1')).resolves.toBe('Олена');
expect(usersRepositoryMock.findById).toHaveBeenCalledWith('1');
});
it('повертає null, якщо користувача не знайдено', async () => {
usersRepositoryMock.findById = jest.fn().mockResolvedValue(null);
await expect(usersService.getUserName('2')).resolves.toBeNull();
expect(usersRepositoryMock.findById).toHaveBeenCalledWith('2');
});
});У цьому прикладі:
UsersService додано до providers.
Реальний репозиторій замінено об’єктом usersRepositoryMock.
Токен USERS_REPOSITORY використано однаково у сервісі та тестовому модулі.
module.get<UsersService>(UsersService) повертає сервіс зі створеною залежністю.
jest.fn() дає змогу контролювати результат виклику репозиторію та перевіряти його аргументи.
useValueuseValue передає готове значення. Це найпростіший спосіб створити мок:
{
provide: USERS_REPOSITORY,
useValue: {
findById: jest.fn(),
},
}Такий підхід зручний, коли тесту потрібен невеликий об’єкт із кількома методами.
useClassuseClass замінює один клас іншим:
{
provide: UsersRepository,
useClass: FakeUsersRepository,
}Приклад фіктивної реалізації:
class FakeUsersRepository {
async findById(id: string) {
return {
id,
name: 'Тестовий користувач',
};
}
}NestJS створить екземпляр FakeUsersRepository і використовуватиме його замість основного класу.
useFactoryuseFactory створює значення за допомогою функції:
{
provide: 'CONFIG',
useFactory: () => ({
environment: 'test',
}),
}Для базових юніт-тестів найчастіше достатньо useValue.
Метод get() приймає токен, за яким провайдер зареєстрований у модулі.
Для класу використовується сам клас:
const usersService = module.get(UsersService);Для рядкового токена використовується цей рядок:
const config = module.get('CONFIG');Для Symbol потрібно передати той самий символ:
const repository = module.get(USERS_REPOSITORY);Тип можна вказати як параметр TypeScript:
const usersService = module.get<UsersService>(UsersService);Це допомагає редактору коду правильно визначати доступні властивості та методи.
TestingModule можна використовувати не лише для сервісів, а й для контролерів. У цьому разі потрібно зареєструвати контролер і його залежності.
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let usersController: UsersController;
const usersServiceMock = {
getUserName: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: usersServiceMock,
},
],
}).compile();
usersController = module.get<UsersController>(UsersController);
});
afterEach(() => {
jest.clearAllMocks();
});
it('створює екземпляр контролера', () => {
expect(usersController).toBeDefined();
});
});Якщо контролер залежить від UsersService, у тестовому модулі потрібно зареєструвати провайдер із токеном UsersService. Це може бути справжній сервіс або мок.
Мок можна оголосити перед тестами та налаштовувати в кожному окремому тесті:
const repositoryMock = {
findById: jest.fn(),
};
beforeEach(() => {
repositoryMock.findById.mockReset();
});Методи Jest дають змогу задати різні результати:
repositoryMock.findById.mockResolvedValue({
id: '1',
name: 'Олена',
});Для помилки:
repositoryMock.findById.mockRejectedValue(
new Error('Помилка сховища'),
);Для одноразового результату:
repositoryMock.findById
.mockResolvedValueOnce({ id: '1', name: 'Олена' })
.mockResolvedValueOnce(null);Це дає змогу перевірити різні сценарії роботи сервісу без зміни його коду.
Тест із TestingModule зазвичай має таку структуру:
Оголосити змінну для тестованого класу.
Створити моки його залежностей.
У beforeEach створити TestingModule.
Зареєструвати тестований клас і моки.
Викликати compile().
Отримати тестований клас через module.get().
У it перевірити конкретний сценарій.
Очистити стан моків після тесту.
describe('SomeService', () => {
let service: SomeService;
const dependencyMock = {
execute: jest.fn(),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
SomeService,
{
provide: SomeDependency,
useValue: dependencyMock,
},
],
}).compile();
service = module.get(SomeService);
});
afterEach(() => {
jest.clearAllMocks();
});
});Якщо сервіс має залежність, але її немає в providers, NestJS не зможе створити сервіс.
await Test.createTestingModule({
providers: [UsersService],
}).compile();Якщо UsersService очікує USERS_REPOSITORY, цей код завершиться помилкою під час компіляції.
Потрібно додати залежність:
await Test.createTestingModule({
providers: [
UsersService,
{
provide: USERS_REPOSITORY,
useValue: {
findById: jest.fn(),
},
},
],
}).compile();Токен у тесті має точно відповідати токену, який використовується в @Inject().
Наприклад, це різні токени:
@Inject(USERS_REPOSITORY)і
@Inject('USERS_REPOSITORY')Якщо в сервісі використано Symbol, у тесті потрібно зареєструвати саме цей Symbol.
await перед compile()compile() повертає проміс, тому модуль потрібно створювати асинхронно:
const module = await Test.createTestingModule({
providers: [UsersService],
}).compile();Юніт-тест має перевіряти один клас. Підключення бази даних робить тест повільнішим і прив’язує його до зовнішньої системи.
Якщо перевіряється логіка UsersService, репозиторій краще замінити мок-об’єктом через useValue.
Якщо мок зберігає виклики або налаштовані результати, вони можуть впливати на наступний тест. Для очищення використовуйте:
afterEach(() => {
jest.clearAllMocks();
});А якщо потрібно повністю скинути налаштування мок-функцій, використовуйте mockReset() для конкретної функції або відповідне налаштування Jest.
TestingModule — ізольований контейнер залежностей для тестів NestJS.
Тестовий модуль створюють через Test.createTestingModule().
Перед використанням модуль потрібно скомпілювати методом compile().
Екземпляр сервісу або контролера отримують через module.get().
Залежності додають до providers.
Реальні залежності можна замінити за допомогою useValue, useClass або useFactory.
Для юніт-тестів найчастіше використовують моки через useValue.
Токен мока має точно збігатися з токеном, який використовує тестований клас.
Створення нового TestingModule для кожного тесту допомагає зберігати ізоляцію перевірок.