Skip to content

Тестирование

Настройка 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"
	}
}

Объяснение

ПараметрЗначениеОписание
rootDirsrcКорневая директория для тестов
testRegex.*\.spec\.ts$Файлы с расширением .spec.ts - это тесты
transformts-jestИспользовать ts-jest для трансформации TypeScript
testEnvironmentnodeОкружение для запуска (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

✅ Правильно

  1. Один тест - один сценарий - Каждый тест проверяет одно
  2. Descriptive names - should return user by id вместо test 1
  3. AAA pattern - Arrange, Act, Assert
  4. Изолируйте тесты - Используйте mocks для зависимостей
  5. Тестируйте граничные случаи - null, пустые массивы, ошибки

❌ Неправильно

  1. Множество проверок в одном тесте - Сложно понять что упало
  2. Зависимости между тестами - Каждый тест должен работать отдельно
  3. Тестирование деталей реализации - Тестируйте поведение
  4. Пропуск edge cases - Тестируйте нормальные и необычные сценарии
  5. Медленные тесты - Используйте 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=4

Coverage низкое

bash
# Увидеть какие строки не покрыты
npm run test:cov

# Открыть в браузере
open coverage/lcov-report/index.html

Следующие шаги