Пошук уроків, статей та іншого контенту
Навчитеся організовувати модульні тести, писати assertions і ізолювати окремі частини коду.
Модульний тест перевіряє невелику ізольовану частину програми: функцію, метод або клас.
У NestJS найчастіше модульні тести пишуть для:
сервісів;
guards;
pipes;
interceptors;
окремих методів контролерів.
Модульний тест не повинен перевіряти всю програму одразу. Його мета — відповісти на конкретне запитання:
Чи повертає цей метод правильний результат для певних вхідних даних?
Наприклад, замість перевірки всього процесу реєстрації користувача можна окремо перевірити, що UsersService:
створює нового користувача;
не створює дубліката;
викликає залежність із правильними аргументами.
У проєктах NestJS зазвичай використовується Jest. Nest CLI додає необхідні налаштування під час створення проєкту.
Файли модульних тестів зазвичай мають суфікс .spec.ts:
users.service.ts
users.service.spec.tsЗапустити всі тести можна командою:
npm testЗапустити тести в режимі спостереження за змінами:
npm run test:watchЗапустити тести з інформацією про покриття:
npm run test:covAssertion — це перевірка очікуваної поведінки програми.
У Jest assertions пишуть за допомогою expect:
expect(actualValue).toBe(expectedValue);Поширені перевірки:
expect(value).toBe(10);
expect(value).toEqual({ id: 1, name: 'Anna' });
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(array).toContain('admin');
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).toHaveBeenCalledWith('test@example.com');Для асинхронних методів можна перевіряти успішний результат:
await expect(service.getUser()).resolves.toEqual(user);Або помилку:
await expect(service.getUser()).rejects.toThrow();toBe і toEqualtoBe зазвичай використовують для примітивних значень:
expect(2 + 2).toBe(4);
expect('NestJS').toBe('NestJS');toEqual порівнює структуру об'єктів і масивів:
expect({ id: 1, name: 'Anna' }).toEqual({
id: 1,
name: 'Anna',
});Сервіс часто залежить від інших частин програми. Наприклад, UsersService може використовувати репозиторій для роботи з базою даних.
Під час модульного тестування не потрібно підключати справжню базу даних. Замість цього створюють mock — контрольовану заміну залежності.
Mock дає змогу:
не виконувати реальні запити;
заздалегідь визначати результат залежності;
перевіряти, чи була залежність викликана;
тестувати лише код поточного класу.
У Jest mock-функцію створюють за допомогою jest.fn():
const findByEmail = jest.fn();Результат mock-функції можна налаштувати:
findByEmail.mockResolvedValue(null);Це означає, що асинхронний виклик поверне null.
Для помилки використовують:
findByEmail.mockRejectedValue(new Error('Database error'));Нехай сервіс реєструє користувачів. Він залежить від репозиторію, але сам репозиторій у модульному тесті буде замінено mock-об'єктом.
users.service.tsimport {
ConflictException,
Inject,
Injectable,
} from '@nestjs/common';
export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
export interface User {
id: number;
email: string;
name: string;
}
export interface UsersRepository {
findByEmail(email: string): Promise<User | null>;
create(data: { email: string; name: string }): Promise<User>;
}
@Injectable()
export class UsersService {
constructor(
@Inject(USERS_REPOSITORY)
private readonly usersRepository: UsersRepository,
) {}
async register(email: string, name: string): Promise<User> {
const existingUser = await this.usersRepository.findByEmail(email);
if (existingUser) {
throw new ConflictException('Користувач уже існує');
}
return this.usersRepository.create({ email, name });
}
}Метод register має дві основні гілки:
користувача з такою електронною адресою ще немає — створити його;
користувач уже існує — викинути помилку.
Обидві гілки потрібно перевірити окремими тестами.
TestingModuleNestJS надає Test.createTestingModule, щоб створити невеликий тестовий модуль.
У ньому можна зареєструвати:
клас, який тестується;
mock-версії його залежностей;
потрібні провайдери.
users.service.spec.tsimport { ConflictException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import {
USERS_REPOSITORY,
User,
UsersRepository,
UsersService,
} from './users.service';
describe('UsersService', () => {
let service: UsersService;
let usersRepository: {
findByEmail: jest.Mock;
create: jest.Mock;
};
beforeEach(async () => {
usersRepository = {
findByEmail: jest.fn(),
create: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: USERS_REPOSITORY,
useValue: usersRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('створює нового користувача', async () => {
const createdUser: User = {
id: 1,
email: 'anna@example.com',
name: 'Anna',
};
usersRepository.findByEmail.mockResolvedValue(null);
usersRepository.create.mockResolvedValue(createdUser);
const result = await service.register(
'anna@example.com',
'Anna',
);
expect(result).toEqual(createdUser);
expect(usersRepository.findByEmail).toHaveBeenCalledWith(
'anna@example.com',
);
expect(usersRepository.create).toHaveBeenCalledWith({
email: 'anna@example.com',
name: 'Anna',
});
});
it('викидає помилку, якщо користувач уже існує', async () => {
const existingUser: User = {
id: 1,
email: 'anna@example.com',
name: 'Anna',
};
usersRepository.findByEmail.mockResolvedValue(existingUser);
await expect(
service.register('anna@example.com', 'Another name'),
).rejects.toThrow(ConflictException);
expect(usersRepository.create).not.toHaveBeenCalled();
});
});У цьому прикладі:
describe групує тести для UsersService;
it описує один окремий сценарій;
beforeEach виконується перед кожним тестом;
useValue передає mock замість справжнього репозиторію;
mockResolvedValue задає результат асинхронної mock-функції;
toHaveBeenCalledWith перевіряє аргументи виклику;
not.toHaveBeenCalled перевіряє, що метод не викликався.
Зручно організовувати тест у три етапи:
Arrange — підготувати дані та mock-залежності;
Act — викликати метод, який тестується;
Assert — перевірити результат.
У прикладі це виглядає так:
// Arrange
usersRepository.findByEmail.mockResolvedValue(null);
usersRepository.create.mockResolvedValue(createdUser);
// Act
const result = await service.register(
'anna@example.com',
'Anna',
);
// Assert
expect(result).toEqual(createdUser);Такий поділ робить тест зрозумілішим і спрощує пошук помилок.
Один тест має перевіряти один логічний сценарій.
Для методу register потрібні щонайменше такі тести:
користувач успішно створюється;
користувач із таким email уже існує;
репозиторій викликається з правильними аргументами;
метод створення не викликається, якщо користувач уже існує.
Не варто об'єднувати всі сценарії в один великий тест. Якщо він завершиться помилкою, буде складніше зрозуміти причину.
У прикладі новий об'єкт mock створюється в beforeEach. Тому кожен тест отримує незалежні mock-функції без попередніх викликів.
Це важливо, оскільки Jest зберігає інформацію про виклики:
expect(usersRepository.create).toHaveBeenCalledTimes(1);Якщо використовувати один mock між тестами, попередній виклик може вплинути на наступний тест.
Коли потрібно скинути налаштування mock-функції, можна використовувати:
mockFunction.mockReset();А щоб очистити лише інформацію про виклики:
mockFunction.mockClear();Для асинхронного методу не слід писати toThrow безпосередньо на результаті виклику. Потрібно передати проміс до expect і використати rejects:
await expect(
service.register('anna@example.com', 'Anna'),
).rejects.toThrow(ConflictException);Також можна перевірити текст помилки:
await expect(
service.register('anna@example.com', 'Anna'),
).rejects.toThrow('Користувач уже існує');Хороший модульний тест перевіряє зовнішню поведінку класу:
результат виконання;
помилки;
виклики важливих залежностей;
аргументи, передані залежностям;
ситуації, коли залежність не повинна викликатися.
Не потрібно перевіряти кожен рядок окремо. Наприклад, немає користі тестувати, що локальна змінна справді отримала значення. Важливіше перевірити кінцевий результат методу.
Модульний тест має бути ізольованим. Підключення справжньої бази даних:
уповільнює тестування;
створює залежність від стану бази;
ускладнює повторюваність тестів.
Для цього використовують mock репозиторію. Реальну взаємодію з базою даних перевіряють іншими типами тестів.
awaitЯкщо тестуєте асинхронний метод, потрібно дочекатися його завершення:
const result = await service.register(
'anna@example.com',
'Anna',
);Без await тест може завершитися раніше, ніж проміс поверне результат.
Для об'єктів потрібно використовувати toEqual, а не toBe:
expect(result).toEqual(expectedUser);toBe перевіряє, чи це буквально той самий об'єкт у пам'яті, а toEqual — чи мають об'єкти однакові дані.
Кожен тест має працювати незалежно від інших. Не слід покладатися на дані або виклики, які залишилися після попереднього тесту.
Створюйте mock-об'єкти в beforeEach або очищуйте їх перед кожним тестом.
Якщо тест перевіряє багато різних сценаріїв, його важко підтримувати. Краще створити кілька коротких тестів із чіткими назвами.
Тест має перевіряти поведінку, а не приватну структуру класу. Якщо внутрішню реалізацію можна змінити без зміни поведінки, модульний тест не повинен через це ламатися.
Модульний тест перевіряє одну ізольовану частину коду.
У NestJS для модульного тестування зазвичай використовують Jest.
expect і matchers використовують для assertions.
TestingModule допомагає створити тестове оточення NestJS.
Залежності краще замінювати mock-об'єктами.
Для асинхронних результатів використовують await, resolves або rejects.
Кожен тест має перевіряти окремий сценарій.
Тести повинні бути незалежними, короткими та зрозумілими.