Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Виды тестов в backend разработке
Это не просто знание терминов, но понимание, когда использовать каждый. Расскажу о пирамиде тестов:
Пирамида тестов (по объему)
E2E Tests (10%)
/ \
/ \
Integration Tests (30%)
/ \
/ \
Unit Tests (60%)
1. Unit тесты (60% от всех)
Определение: Тестируешь отдельную функцию/метод изолированно
// Функция для тестирования
function calculateDiscount(price, discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid discount');
}
return price * (1 - discountPercent / 100);
}
// Unit тест
describe('calculateDiscount', () => {
it('should apply discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('should throw on invalid discount', () => {
expect(() => calculateDiscount(100, 150)).toThrow();
});
it('should handle 0 discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
});
Характеристики:
- Быстрые (< 100ms каждый)
- Не нужна БД, сеть
- Мокируешь dependencies
- Легко писать и поддерживать
Best practices:
// ✅ ХОРОШО: Arrange-Act-Assert pattern
describe('User service', () => {
it('should create user with valid email', () => {
// Arrange
const userService = new UserService();
const userData = { email: 'test@example.com', name: 'John' };
// Act
const result = userService.validate(userData);
// Assert
expect(result.isValid).toBe(true);
});
});
// ❌ ПЛОХО: Слишком общее утверждение
it('should work', () => {
expect(calculateDiscount(100, 20)).toBeTruthy();
});
2. Integration тесты (30% от всех)
Определение: Тестируешь взаимодействие между компонентами (например, controller + service + DB)
// Тестирование API endpoint с БД
describe('POST /api/users', () => {
let db;
beforeAll(async () => {
db = new Database();
await db.connect();
});
afterAll(async () => {
await db.disconnect();
});
afterEach(async () => {
await db.clearTable('users');
});
it('should create user in database', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'john@example.com', name: 'John' });
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
// Проверяем, что данные в БД
const user = await db.users.findOne({ email: 'john@example.com' });
expect(user).toBeDefined();
expect(user.name).toBe('John');
});
it('should reject duplicate email', async () => {
await request(app)
.post('/api/users')
.send({ email: 'john@example.com', name: 'John' });
const response = await request(app)
.post('/api/users')
.send({ email: 'john@example.com', name: 'Jane' });
expect(response.status).toBe(409); // Conflict
});
});
Характеристики:
- Медленнее unit тестов (1-5 сек)
- Используешь реальную/тестовую БД
- Можешь мокировать external APIs
- Проверяешь happy path и error cases
Когда нужны:
- API endpoints
- Database interactions
- Service layer logic
3. E2E тесты (10% от всех)
Определение: Тестируешь entire user flow от старта до конца
// Пример с Playwright
describe('User registration flow', () => {
it('should complete user registration', async () => {
const { page } = await initBrowser();
// Navigate to registration
await page.goto('http://localhost:3000/register');
// Fill form
await page.fill('input[name="email"]', 'newuser@example.com');
await page.fill('input[name="password"]', 'SecurePassword123');
await page.fill('input[name="confirmPassword"]', 'SecurePassword123');
// Submit
await page.click('button[type="submit"]');
// Check redirect to dashboard
await page.waitForNavigation();
expect(page.url()).toContain('/dashboard');
// Verify user is logged in
const userName = await page.textContent('[data-testid="user-name"]');
expect(userName).toBe('newuser@example.com');
});
});
Характеристики:
- Очень медленные (10-60 сек)
- Тестируют реальный browser
- Проверяют UI, interactions
- Только critical paths
4. Contract тесты (emerging)
Определение: Тестируешь контракт между сервисами (например, API response format)
// Микросервис A: Users Service
describe('GET /api/users/:id contract', () => {
it('should return user with required fields', async () => {
const response = await request(app)
.get('/api/users/1');
expect(response.body).toMatchObject({
id: expect.any(Number),
email: expect.any(String),
name: expect.any(String),
createdAt: expect.any(String), // ISO format
});
});
});
// Микросервис B: Orders Service (консумирует Users API)
describe('Users Service contract', () => {
it('should receive user data in expected format', async () => {
// Используешь mock из Users Service
const userProvider = require('pact').Pact(...);
userProvider.addInteraction({
state: 'user with id 1 exists',
uponReceiving: 'a request for user 1',
withRequest: { method: 'GET', path: '/api/users/1' },
willRespondWith: {
status: 200,
body: {
id: 1,
email: 'john@example.com',
name: 'John Doe',
createdAt: '2024-01-01T00:00:00Z'
}
}
});
});
});
5. Load тесты (performance)
Определение: Тестируешь приложение под нагрузкой
// k6 script
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Stay
{ duration: '2m', target: 0 }, // Ramp down
],
};
export default function() {
const response = http.get('http://localhost:3000/api/users/1');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
}
6. Security тесты
Определение: Проверяешь защиту от common vulnerabilities
describe('Security tests', () => {
it('should prevent SQL injection', async () => {
const response = await request(app)
.get('/api/users/1 OR 1=1');
expect(response.status).toBe(400);
});
it('should validate JWT token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid_token');
expect(response.status).toBe(401);
});
it('should not expose sensitive headers', async () => {
const response = await request(app).get('/');
expect(response.headers['x-powered-by']).toBeUndefined();
});
});
7. Snapshot тесты (осторожно!)
Определение: Сохраняешь output и сравниваешь с будущими версиями
// Можно, но опасно
it('should return expected user object', () => {
const user = new User({ id: 1, name: 'John' });
expect(user).toMatchSnapshot();
});
// Более безопасно: проверяй specific fields
it('should return user with correct fields', () => {
const user = new User({ id: 1, name: 'John' });
expect(user).toEqual({
id: 1,
name: 'John',
createdAt: expect.any(Date)
});
});
8. Mutation тесты (продвинутый уровень)
Определение: Инструмент ломает код и проверяет, упадут ли тесты
npm install stryker
stryker run
Если Stryker может удалить строку кода и тесты всё равно проходят → твой тест слабый.
Рекомендуемое распределение
Всего тестов:
- 60% Unit tests (быстрые, many)
- 30% Integration tests (medium speed, few)
- 10% E2E tests (медленные, only critical flows)
- Contract tests (between microservices)
- Load tests (перед production release)
Tools, которые я использую
Unit/Integration:
- jest (самый популярный)
- mocha + chai
- vitest (faster than jest)
E2E:
- Playwright (самый modern)
- Cypress (frontend-heavy)
- Selenium (legacy, но всё ещё work)
Load:
- k6 (самый easy to use)
- wrk (для http нагрузки)
- Apache JMeter (oldschool, но powerful)
Monitoring:
- Istanbul/nyc (coverage reports)
- Sonarqube (code quality)
Итог
Жизнь без тестов = playing with fire в production. Каждый тип тестов решает свою проблему:
- Unit тесты = быстрая feedback при разработке
- Integration тесты = реальные сценарии работают
- E2E тесты = user не сломает приложение
- Load тесты = масштабируемость гарантирована