Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегия тестирования в backend проектах
Тестирование — это критически важная часть качественной разработки. За 10+ лет я разработал comprehensive testing strategy для Node.js проектов, которая обеспечивает надёжность и maintainability кода.
Пирамида тестирования
E2E Tests
/ \
Integration Tests
/ \
Unit Tests
/ \
Build & Lint
Распределение по количеству:
- Unit тесты — 70% (быстро, изолировано)
- Integration тесты — 20% (проверяют взаимодействие)
- E2E тесты — 10% (полный flow, медленнее)
1. Unit тесты (Jest/Vitest)
Единичные тесты для отдельных функций и классов.
// src/services/user.service.ts
export class UserService {
constructor(private userRepository: UserRepository) {}
async createUser(email: string, password: string): Promise<User> {
if (!this.isValidEmail(email)) {
throw new Error('Invalid email');
}
const hashedPassword = await this.hashPassword(password);
return this.userRepository.create({ email, password: hashedPassword });
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
private async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
// src/services/user.service.test.ts
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
describe('UserService', () => {
let service: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepository = {
create: jest.fn(),
} as any;
service = new UserService(mockRepository);
});
describe('createUser', () => {
it('should create user with valid email and password', async () => {
// Arrange
const email = 'test@example.com';
const password = 'secure123';
const expectedUser = { id: '1', email, password: 'hashed' };
mockRepository.create.mockResolvedValue(expectedUser);
// Act
const result = await service.createUser(email, password);
// Assert
expect(result).toEqual(expectedUser);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({ email })
);
});
it('should throw error for invalid email', async () => {
// Arrange & Act & Assert
await expect(service.createUser('invalid-email', 'password123'))
.rejects
.toThrow('Invalid email');
expect(mockRepository.create).not.toHaveBeenCalled();
});
it('should hash password before saving', async () => {
// Arrange
const email = 'test@example.com';
const password = 'mypassword';
mockRepository.create.mockResolvedValue({
id: '1',
email,
password: expect.stringMatching(/^\$2[aby]\$/), // bcrypt hash pattern
});
// Act
await service.createUser(email, password);
// Assert
const callArgs = mockRepository.create.mock.calls[0][0];
expect(callArgs.password).not.toBe(password); // Не исходный пароль
});
});
});
Ключевые моменты:
- AAA Pattern (Arrange, Act, Assert)
- Моки для зависимостей (mockRepository)
- Граничные случаи (invalid email)
- Проверка побочных эффектов (was repository.create called?)
2. Integration тесты
Тестируют взаимодействие компонентов (сервис + репозиторий + БД).
// src/services/user.service.integration.test.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
describe('UserService Integration', () => {
let service: UserService;
let repository: Repository<User>;
let dataSource: DataSource;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UserService, UserRepository],
}).compile();
service = moduleRef.get<UserService>(UserService);
repository = moduleRef.get<Repository<User>>(
getRepositoryToken(User)
);
dataSource = moduleRef.get<DataSource>(DataSource);
});
afterEach(async () => {
// Очистка БД после каждого теста
await repository.clear();
});
afterAll(async () => {
await dataSource.destroy();
});
it('should create user and persist to database', async () => {
// Arrange
const email = 'integration@test.com';
const password = 'secure123';
// Act
const user = await service.createUser(email, password);
// Assert
const savedUser = await repository.findOne({ where: { id: user.id } });
expect(savedUser).toBeDefined();
expect(savedUser?.email).toBe(email);
});
it('should not create duplicate emails', async () => {
// Arrange
const email = 'duplicate@test.com';
await service.createUser(email, 'password1');
// Act & Assert
await expect(service.createUser(email, 'password2'))
.rejects
.toThrow('Email already exists');
});
});
Особенности:
- Реальная БД (SQLite in-memory для тестов)
- Полный lifecycle (создание, сохранение, поиск)
- Проверка ограничений (unique email)
3. E2E тесты (API testing)
Тестируют полный HTTP flow от request до response.
// src/users/users.controller.e2e.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { UsersModule } from './users.module';
describe('Users API (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('POST /api/v1/users (Create user)', () => {
it('should create user with valid data', async () => {
const createUserDto = {
email: 'newuser@test.com',
password: 'SecurePassword123!',
name: 'John Doe',
};
const response = await request(app.getHttpServer())
.post('/api/v1/users')
.send(createUserDto)
.expect(201);
expect(response.body).toEqual(
expect.objectContaining({
id: expect.any(String),
email: createUserDto.email,
name: createUserDto.name,
})
);
// Важно: пароль НЕ возвращается в ответе!
expect(response.body.password).toBeUndefined();
});
it('should validate email format', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/users')
.send({
email: 'invalid-email',
password: 'SecurePassword123!',
name: 'Jane',
})
.expect(400);
expect(response.body.message).toContain('Invalid email');
});
it('should return 409 for duplicate email', async () => {
const email = 'existing@test.com';
const dto = { email, password: 'pass123', name: 'User' };
// Первое создание
await request(app.getHttpServer())
.post('/api/v1/users')
.send(dto)
.expect(201);
// Второе с тем же email
const response = await request(app.getHttpServer())
.post('/api/v1/users')
.send(dto)
.expect(409);
expect(response.body.message).toContain('Email already exists');
});
});
describe('GET /api/v1/users/:id (Get user)', () => {
let userId: string;
beforeAll(async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/users')
.send({
email: 'getuser@test.com',
password: 'pass123',
name: 'Get Test',
});
userId = response.body.id;
});
it('should get user by id', async () => {
const response = await request(app.getHttpServer())
.get(`/api/v1/users/${userId}`)
.expect(200);
expect(response.body).toEqual(
expect.objectContaining({
id: userId,
email: 'getuser@test.com',
name: 'Get Test',
})
);
});
it('should return 404 for non-existent user', async () => {
const fakeId = 'non-existent-id';
await request(app.getHttpServer())
.get(`/api/v1/users/${fakeId}`)
.expect(404);
});
});
});
4. Database тесты (с миграциями)
// test/database.setup.ts
import { DataSource } from 'typeorm';
import { runMigrations, revertMigrations } from '../migrations';
export async function setupTestDatabase(): Promise<DataSource> {
const dataSource = new DataSource({
type: 'postgres',
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT || '5432'),
username: process.env.TEST_DB_USER || 'test',
password: process.env.TEST_DB_PASSWORD || 'test',
database: process.env.TEST_DB_NAME || 'testdb',
entities: [/* ... */],
dropSchema: true, // Очистить перед запуском
synchronize: false,
migrationsRun: true,
migrations: ['migrations/*.ts'],
});
await dataSource.initialize();
return dataSource;
}
// Использование
beforeAll(async () => {
dataSource = await setupTestDatabase();
});
afterAll(async () => {
await dataSource.destroy();
});
5. Тестирование асинхронного кода
describe('Async operations', () => {
it('should handle promise', async () => {
const promise = service.fetchUser(123);
const user = await promise;
expect(user.id).toBe(123);
});
it('should handle timeout', async () => {
jest.useFakeTimers();
const promise = service.delayedOperation();
// Пропускаем время
jest.advanceTimersByTime(5000);
expect(promise).resolves.toBeDefined();
jest.useRealTimers();
});
it('should retry on failure', async () => {
const spy = jest.spyOn(service, 'unreliableApi');
spy.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ data: 'success' });
const result = await service.apiWithRetry();
expect(result.data).toBe('success');
expect(spy).toHaveBeenCalledTimes(2);
});
});
6. Coverage requirements
# package.json
"scripts": {
"test": "jest --coverage",
"test:watch": "jest --watch"
}
# jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
7. Fixtures и test data
// test/fixtures/users.fixture.ts
export const validUserData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User',
};
export const invalidUsers = [
{ email: 'invalid', password: 'pass' }, // Invalid email
{ email: 'test@test.com', password: '' }, // Empty password
{ email: '', password: 'pass' }, // Empty email
];
// Использование
it('should reject invalid users', async () => {
for (const invalidUser of invalidUsers) {
await expect(service.createUser(
invalidUser.email,
invalidUser.password
)).rejects.toThrow();
}
});
8. Тестирование HTTP клиентов (VCR.py / nock)
// При первом запуске записывает реальный HTTP ответ
// При последующих запусках использует записанные данные
import nock from 'nock';
it('should call external API', async () => {
nock('https://api.external.com')
.get('/data/123')
.reply(200, { id: 123, data: 'mocked response' });
const result = await externalService.fetchData(123);
expect(result.data).toBe('mocked response');
});
Чеклист для тестирования проекта
✅ Структура:
- Unit тесты для каждого сервиса
- Integration тесты для сложного взаимодействия
- E2E тесты для API endpoints
- Фиксчуры для test data
✅ Coverage:
- Минимум 80% для production кода
- 100% для критичных операций (auth, payment)
✅ Скорость:
- Unit тесты < 100ms
- Integration < 1s
- E2E тесты группировать (max 10s per suite)
✅ Мейнтейнанс:
- Descriptive test names
- One assertion per test (обычно)
- Clear setup/teardown
- Изолированность (не зависят от порядка)
✅ CI/CD интеграция:
- Автоматический запуск на каждый commit
- Fail если coverage < threshold
- Reportы по результатам
Итоги
Качественное тестирование: ✓ Предотвращает баги в production ✓ Упрощает рефакторинг ✓ Служит документацией ✓ Даёт уверенность в коде ✓ Экономит время при поддержке
Я никогда не пишу production код без тестов — это стандарт профессиональной разработки.