← Назад к вопросам
Как реализованы тесты на проекте?
2.0 Middle🔥 291 комментариев
#Soft skills и опыт работы#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализованы тесты на проекте?
Тестирование — не хобби, а обязательная часть разработки. На моем проекте используется многоуровневая стратегия тестирования с высокой coverage и fast feedback loop. Покажу реальный пример архитектуры.
Pyramid Testing: правильная структура
Использую тестовую пирамиду:
┌─────────────────┐
│ E2E Тесты │ 5% (Google Chrome)
├─────────────────┤
│ Integration │ 20% (API, БД, очереди)
├─────────────────┤
│ Unit │ 75% (функции, классы)
└─────────────────┘
Unit тесты (75%)
- Быстрые (миллисекунды)
- Изолированные (mock зависимости)
- Дешевые в поддержке
- Запускаются на CI за 10 секунд
Integration тесты (20%)
- Проверяют взаимодействие с БД, кэшем, очередями
- Более медленные, но все еще быстрые
- Запускаются на CI за 30-60 секунд
E2E тесты (5%)
- Тестируют весь workflow (API -> БД -> сообщения)
- Самые медленные
- Запускаются ночью или перед релизом
1. Unit тесты с Jest
// src/services/__tests__/UserService.test.ts
import { UserService } from '../UserService';
import { UserRepository } from '../../repositories/UserRepository';
describe('UserService', () => {
let userService: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
// Mock репозитория
mockRepository = {
findById: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
} as any;
userService = new UserService(mockRepository);
});
describe('getUserProfile', () => {
it('should return user profile if user exists', async () => {
// Arrange
const userId = 'user-123';
const userData = {
id: userId,
email: 'user@example.com',
name: 'John Doe',
};
mockRepository.findById.mockResolvedValue(userData);
// Act
const result = await userService.getUserProfile(userId);
// Assert
expect(result).toEqual(userData);
expect(mockRepository.findById).toHaveBeenCalledWith(userId);
expect(mockRepository.findById).toHaveBeenCalledTimes(1);
});
it('should throw NotFoundException if user does not exist', async () => {
// Arrange
const userId = 'non-existent';
mockRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(userService.getUserProfile(userId)).rejects.toThrow(
'User not found'
);
});
it('should handle database errors gracefully', async () => {
// Arrange
mockRepository.findById.mockRejectedValue(
new Error('Database connection failed')
);
// Act & Assert
await expect(userService.getUserProfile('any')).rejects.toThrow(
'Database connection failed'
);
});
});
describe('updateUserEmail', () => {
it('should update email and return updated user', async () => {
const userId = 'user-123';
const newEmail = 'newemail@example.com';
const updatedUser = {
id: userId,
email: newEmail,
name: 'John Doe',
};
mockRepository.findById.mockResolvedValue({
id: userId,
email: 'old@example.com',
name: 'John Doe',
});
mockRepository.save.mockResolvedValue(updatedUser);
// Act
const result = await userService.updateUserEmail(userId, newEmail);
// Assert
expect(result.email).toBe(newEmail);
expect(mockRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
id: userId,
email: newEmail,
})
);
});
});
});
2. Integration тесты с тестовой БД
// src/services/__tests__/UserService.integration.test.ts
import { Database } from '../../database/Database';
import { UserRepository } from '../../repositories/UserRepository';
import { UserService } from '../UserService';
describe('UserService Integration', () => {
let db: Database;
let userRepository: UserRepository;
let userService: UserService;
// beforeAll запускается один раз перед всеми тестами
beforeAll(async () => {
db = new Database({
host: 'localhost',
database: process.env.TEST_DB_NAME || 'test_db',
// ...
});
await db.connect();
await db.runMigrations(); // Накатываем миграции
});
// afterAll убирает за собой
afterAll(async () => {
await db.rollbackMigrations(); // Откатываем миграции
await db.disconnect();
});
// beforeEach очищает данные перед каждым тестом
beforeEach(async () => {
await db.truncateAllTables();
});
it('should create user and retrieve from database', async () => {
// Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'hashed_password',
};
// Act
const createdUser = await userRepository.create(userData);
// Assert
const foundUser = await userRepository.findById(createdUser.id);
expect(foundUser).toEqual(expect.objectContaining(userData));
expect(foundUser.id).toBeDefined();
});
it('should update user and persist changes', async () => {
// Arrange
const user = await userRepository.create({
email: 'test@example.com',
name: 'Original Name',
password: 'hashed',
});
// Act
await userRepository.save({
...user,
name: 'Updated Name',
});
// Assert
const updated = await userRepository.findById(user.id);
expect(updated.name).toBe('Updated Name');
});
it('should enforce unique email constraint', async () => {
// Arrange
const email = 'unique@example.com';
await userRepository.create({
email,
name: 'User 1',
password: 'hashed',
});
// Act & Assert
await expect(
userRepository.create({
email,
name: 'User 2',
password: 'hashed',
})
).rejects.toThrow('Unique constraint violation');
});
});
3. API Integration тесты
// src/api/__tests__/users.e2e.test.ts
import request from 'supertest';
import { createApp } from '../../app';
import { Database } from '../../database/Database';
describe('POST /api/v1/users', () => {
let app;
let db: Database;
beforeAll(async () => {
app = createApp();
db = new Database({ host: 'localhost', database: 'test_db' });
await db.connect();
await db.runMigrations();
});
beforeEach(async () => {
await db.truncateAllTables();
});
afterAll(async () => {
await db.rollbackMigrations();
await db.disconnect();
});
it('should create user with valid data', async () => {
// Act
const response = await request(app)
.post('/api/v1/users')
.send({
email: 'newuser@example.com',
name: 'New User',
password: 'SecurePassword123',
})
.expect(201); // Проверяем status code
// Assert
expect(response.body).toHaveProperty('id');
expect(response.body.email).toBe('newuser@example.com');
expect(response.body).not.toHaveProperty('password'); // Пароль не должен возвращаться
});
it('should return 400 if email is invalid', async () => {
// Act & Assert
await request(app)
.post('/api/v1/users')
.send({
email: 'invalid-email',
name: 'User',
password: 'password',
})
.expect(400);
});
it('should return 409 if email already exists', async () => {
// Arrange
const email = 'existing@example.com';
await request(app)
.post('/api/v1/users')
.send({
email,
name: 'First User',
password: 'password123',
});
// Act & Assert
await request(app)
.post('/api/v1/users')
.send({
email,
name: 'Second User',
password: 'password123',
})
.expect(409);
});
});
4. Асинхронные очереди - тестирование с VCR
// src/services/__tests__/EmailService.test.ts
import nock from 'nock';
import { EmailService } from '../EmailService';
describe('EmailService with HTTP calls', () => {
let emailService: EmailService;
beforeEach(() => {
emailService = new EmailService('https://api.sendgrid.com');
});
afterEach(() => {
nock.cleanAll(); // Убираем все mocks
});
it('should send email via SendGrid API', async () => {
// Arrange: мокируем HTTP запрос
nock('https://api.sendgrid.com')
.post('/v3/mail/send', {
personalizations: [{ to: [{ email: 'user@example.com' }] }],
from: { email: 'noreply@app.com' },
subject: 'Welcome',
content: [{ type: 'text/html', value: 'Welcome!' }],
})
.reply(202); // SendGrid возвращает 202 при успехе
// Act
const result = await emailService.send({
to: 'user@example.com',
subject: 'Welcome',
html: 'Welcome!',
});
// Assert
expect(result.status).toBe('sent');
});
it('should handle SendGrid API errors', async () => {
// Arrange
nock('https://api.sendgrid.com')
.post('/v3/mail/send')
.reply(400, { errors: [{ message: 'Invalid email' }] });
// Act & Assert
await expect(
emailService.send({
to: 'invalid-email',
subject: 'Test',
html: 'Test',
})
).rejects.toThrow('Invalid email');
});
});
5. Redis / Cache тесты
// src/services/__tests__/CacheService.test.ts
import Redis from 'ioredis';
import { CacheService } from '../CacheService';
describe('CacheService', () => {
let redis: Redis;
let cacheService: CacheService;
beforeAll(async () => {
redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
db: 15, // Используем отдельную БД для тестов
});
cacheService = new CacheService(redis);
});
beforeEach(async () => {
await redis.flushdb(); // Очищаем БД перед каждым тестом
});
afterAll(async () => {
await redis.disconnect();
});
it('should cache value and retrieve it', async () => {
// Arrange
const key = 'user:123';
const value = { id: 123, name: 'John' };
// Act
await cacheService.set(key, value, 3600);
const result = await cacheService.get(key);
// Assert
expect(result).toEqual(value);
});
it('should expire cache after TTL', async () => {
// Arrange
await cacheService.set('temp', 'value', 1); // TTL 1 сек
// Act
await new Promise(resolve => setTimeout(resolve, 1100)); // Ждем 1.1 сек
const result = await cacheService.get('temp');
// Assert
expect(result).toBeNull();
});
});
6. Тестирование с Mocking библиотеками
// Mock модуля
jest.mock('../../external/PaymentGateway', () => ({
PaymentGateway: {
charge: jest.fn(),
},
}));
import { PaymentGateway } from '../../external/PaymentGateway';
describe('PaymentService', () => {
it('should process payment successfully', async () => {
(PaymentGateway.charge as jest.Mock).mockResolvedValue({
transactionId: 'tx-123',
status: 'success',
});
const result = await paymentService.processPayment(100, 'card-token');
expect(result.status).toBe('success');
expect(PaymentGateway.charge).toHaveBeenCalledWith(100, 'card-token');
});
});
7. Coverage отчет
# Запуск с coverage
npm test -- --coverage
# Результат:
# Statements : 92.5% ( 445/482 )
# Branches : 88.3% ( 234/265 )
# Functions : 90.1% ( 91/101 )
# Lines : 93.2% ( 401/430 )
Правило: coverage >= 85%, ideally 90%+. Но это не золотой стандарт — важнее качество тестов, чем их количество.
8. CI/CD интеграция
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_DB: test_db
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run lint
- run: npm test -- --coverage
- run: npm run test:integration
# Upload coverage report
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
9. Best Practices
1. Тестируй поведение, а не реализацию
// ❌ Плохо: тестируем реализацию
expect(userService.cache).toBeDefined();
// ✅ Хорошо: тестируем поведение
const user1 = await userService.getUser('123');
const user2 = await userService.getUser('123');
expect(mockRepository.findById).toHaveBeenCalledTimes(1); // Кэш работает
2. Используй descriptive names
// ❌ Плохо
it('works', async () => {});
// ✅ Хорошо
it('should return cached user on second request without hitting database', async () => {});
3. Используй AAA pattern (Arrange-Act-Assert)
it('should update user email', async () => {
// Arrange
const user = { id: '123', email: 'old@example.com' };
// Act
await userService.updateEmail(user.id, 'new@example.com');
// Assert
const updated = await userRepository.findById(user.id);
expect(updated.email).toBe('new@example.com');
});
Итог
Тестирование — это инвестиция в качество. На первый взгляд медленнее, но:
- Легче рефакторить без страха что-то сломать
- Меньше ошибок в production
- Код становится лучше (если пишешь тесты до кода — TDD)
- Новичкам легче разобраться в коде через тесты
Мой подход: Unit + Integration = 95%, E2E = 5%. Это баланс скорости обратной связи и надежности.