Тестирование
Настройка Jest и написание тестов для обеспечения качества кода.
Обзор
Проект использует Jest для unit тестирования и охвата кода.
- Версия: Jest 29
- Конфиг:
package.json - Coverage Reporter: Istanbul (default Jest)
Jest конфигурация
Настройки в package.json
json
{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": ["**/*.(t|j)s"],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}Объяснение
| Параметр | Значение | Описание |
|---|---|---|
rootDir | src | Корневая директория для тестов |
testRegex | .*\.spec\.ts$ | Файлы с расширением .spec.ts - это тесты |
transform | ts-jest | Использовать ts-jest для трансформации TypeScript |
testEnvironment | node | Окружение для запуска (Node.js, не браузер) |
coverageDirectory | ../coverage | Путь для сохранения отчета о покрытии |
Структура тестов
Именование файлов
services/
├── users.service.ts # Основной файл
└── users.service.spec.ts # Файл с тестами (рядом)Тестовый файл должен быть рядом с основным файлом и иметь суффикс .spec.ts.
Структура тестового файла
typescript
import { Test, TestingModule } from "@nestjs/testing";
import { UsersService } from "./users.service";
describe("UsersService", () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService]
}).compile();
service = module.get<UsersService>(UsersService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("findOne", () => {
it("should return a user", async () => {
// Тестовый код
});
});
});Команды Jest
Запустить тесты
bash
# Запустить все тесты
npm test
# Запустить в режиме наблюдения (watch mode)
npm test -- --watch
# Запустить конкретный файл
npm test -- users.service.spec.ts
# Запустить тесты с паттерном
npm test -- --testNamePattern="findOne"Coverage отчет
bash
# Запустить тесты с сбором coverage
npm run test:cov
# Coverage отчет будет в coverage/
# Открыть в браузере: coverage/lcov-report/index.htmlНаписание тестов
1. Unit тест сервиса
typescript
import { Test, TestingModule } from "@nestjs/testing";
import { getRepositoryToken } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UsersService } from "./users.service";
import { UserEntity } from "./user.entity";
describe("UsersService", () => {
let service: UsersService;
let repository: Repository<UserEntity>;
beforeEach(async () => {
// Mock repository
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn()
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(UserEntity),
useValue: mockRepository
}
]
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<UserEntity>>(getRepositoryToken(UserEntity));
});
describe("findOne", () => {
it("should return a user by id", async () => {
const user = { id: "1", email: "test@example.com" };
jest.spyOn(repository, "findOne").mockResolvedValue(user);
const result = await service.findOne({ where: { id: "1" } });
expect(result).toEqual(user);
expect(repository.findOne).toHaveBeenCalledWith({ where: { id: "1" } });
});
it("should return null if user not found", async () => {
jest.spyOn(repository, "findOne").mockResolvedValue(null);
const result = await service.findOne({ where: { id: "invalid" } });
expect(result).toBeNull();
});
});
describe("create", () => {
it("should create a user", async () => {
const userData = { email: "new@example.com", password: "hash" };
const savedUser = { id: "1", ...userData };
jest.spyOn(repository, "save").mockResolvedValue(savedUser);
jest.spyOn(repository, "findOneBy").mockResolvedValue(savedUser);
const result = await service.create(userData);
expect(result).toEqual(savedUser);
expect(repository.save).toHaveBeenCalledWith(userData);
});
});
});2. Unit тест контроллера
typescript
import { Test, TestingModule } from "@nestjs/testing";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
describe("UsersController", () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const mockUsersService = {
findOne: jest.fn(),
create: jest.fn()
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService
}
]
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
describe("getUser", () => {
it("should return a user", async () => {
const user = { id: "1", email: "test@example.com" };
jest.spyOn(service, "findOne").mockResolvedValue(user);
const result = await controller.getUser("1");
expect(result).toEqual(user);
});
});
describe("createUser", () => {
it("should create a user", async () => {
const dto = { email: "new@example.com", password: "pass123" };
const created = { id: "1", ...dto };
jest.spyOn(service, "create").mockResolvedValue(created);
const result = await controller.createUser(dto);
expect(result).toEqual(created);
});
});
});3. Test с fixtures
typescript
describe("UsersService", () => {
// Используем fixtures (тестовые данные)
const testUser = {
id: "1",
email: "test@example.com",
password: "hashed_password",
createdAt: new Date(),
updatedAt: new Date()
};
describe("findOne", () => {
it("should handle user not found gracefully", async () => {
jest.spyOn(repository, "findOne").mockResolvedValue(null);
const result = await service.findOne({ where: { id: "invalid" } });
expect(result).toBeNull();
});
});
});Mocking
Mock функции
typescript
// Mock метода
jest.spyOn(service, "method").mockResolvedValue(value);
// Mock с реализацией
jest.spyOn(service, "method").mockImplementation((arg) => {
return arg * 2;
});
// Mock с ошибкой
jest.spyOn(service, "method").mockRejectedValue(new Error("Test error"));
// Mock с разными возвращаемыми значениями
jest.spyOn(service, "method").mockResolvedValueOnce(value1).mockResolvedValueOnce(value2);Mock модулей
typescript
jest.mock("./users.service", () => ({
UsersService: jest.fn(() => ({
findOne: jest.fn(),
create: jest.fn()
}))
}));Mock Repository (TypeORM)
typescript
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
findAndCount: jest.fn()
};Assertions
Проверка результатов
typescript
// Проверка на равенство
expect(result).toEqual(expected);
expect(result).toBe(value);
// Проверка типа
expect(result).toBeDefined();
expect(result).toBeNull();
expect(result).toBeTruthy();
expect(result).toBeFalsy();
// Проверка массивов
expect(result).toHaveLength(3);
expect(result).toContain(item);
// Проверка объектов
expect(result).toHaveProperty("id");
expect(result).toMatchObject({ email: "test@test.com" });Проверка вызовов
typescript
// Проверка что функция была вызвана
expect(service.method).toHaveBeenCalled();
// Проверка с какими аргументами
expect(service.method).toHaveBeenCalledWith(arg1, arg2);
// Проверка количество раз
expect(service.method).toHaveBeenCalledTimes(1);Best Practices
✅ Правильно
- Один тест - один сценарий - Каждый тест проверяет одно
- Descriptive names -
should return user by idвместоtest 1 - AAA pattern - Arrange, Act, Assert
- Изолируйте тесты - Используйте mocks для зависимостей
- Тестируйте граничные случаи - null, пустые массивы, ошибки
❌ Неправильно
- Множество проверок в одном тесте - Сложно понять что упало
- Зависимости между тестами - Каждый тест должен работать отдельно
- Тестирование деталей реализации - Тестируйте поведение
- Пропуск edge cases - Тестируйте нормальные и необычные сценарии
- Медленные тесты - Используйте mocks вместо реальных БД
Coverage целевые значения
Statements : 80%+ # Процент выполненных строк
Branches : 75%+ # Процент покрытия условных ветвей
Functions : 80%+ # Процент покрытия функций
Lines : 80%+ # Процент покрытых строкTroubleshooting
Тесты не запускаются
bash
# Проверить конфигурацию
npm test -- --version
# Перестроить
npm install
# Запустить конкретный тест
npm test -- users.service.spec.tsТесты медленные
bash
# Запустить только измененные файлы
npm test -- --onlyChanged
# Запустить параллельно
npm test -- --maxWorkers=4Coverage низкое
bash
# Увидеть какие строки не покрыты
npm run test:cov
# Открыть в браузере
open coverage/lcov-report/index.htmlСледующие шаги
- Git Workflow - Процесс коммитов
- Линтинг - ESLint и Prettier
- Правила кода - Стандарты написания