Пирамида тестирования
Эффективная стратегия тестирования строится на пирамиде: много unit тестов, меньше интеграционных, ещё меньше e2e. Это обеспечивает баланс между скоростью и покрытием кода.
- Unit тесты — тестируют отдельные функции и компоненты
- Интеграционные тесты — проверяют взаимодействие модулей
- E2E тесты — имитируют действия пользователя
Unit тесты с Vitest
Vitest — современный фреймворк для тестирования, совместимый с Jest API:
// utils/validation.ts
export function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function formatPrice(price: number, currency = 'RUB'): string {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency
}).format(price);
}
// utils/validation.test.ts
import { describe, it, expect } from 'vitest';
import { validateEmail, formatPrice } from './validation';
describe('validateEmail', () => {
it('should return true for valid emails', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('user.name@domain.org')).toBe(true);
});
it('should return false for invalid emails', () => {
expect(validateEmail('invalid')).toBe(false);
expect(validateEmail('no@')).toBe(false);
expect(validateEmail('@domain.com')).toBe(false);
});
});
describe('formatPrice', () => {
it('should format price in RUB by default', () => {
expect(formatPrice(1000)).toContain('1');
expect(formatPrice(1000)).toContain('000');
});
it('should format price in specified currency', () => {
const result = formatPrice(100, 'USD');
expect(result).toContain('$');
});
});
Тестирование React компонентов
// components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
}
export function Button({ children, onClick, disabled, loading }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled || loading}
className="btn"
>
{loading ? 'Loading...' : children}
</button>
);
}
// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('is disabled when loading', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Интеграционные тесты
// api/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createServer } from '../server';
import { prisma } from '../lib/prisma';
let server: any;
beforeAll(async () => {
server = await createServer();
await server.listen(0);
});
afterAll(async () => {
await server.close();
await prisma.$disconnect();
});
describe('Users API', () => {
it('GET /api/users returns list of users', async () => {
const response = await fetch(`http://localhost:${server.port}/api/users`);
const data = await response.json();
expect(response.status).toBe(200);
expect(Array.isArray(data)).toBe(true);
});
it('POST /api/users creates new user', async () => {
const response = await fetch(`http://localhost:${server.port}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com'
})
});
const user = await response.json();
expect(response.status).toBe(201);
expect(user.name).toBe('Test User');
expect(user.id).toBeDefined();
});
});
E2E тесты с Playwright
Playwright — мощный инструмент для end-to-end тестирования:
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('user can login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'wrong@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Invalid credentials');
});
});
test.describe('Shopping Cart', () => {
test('user can add product to cart', async ({ page }) => {
await page.goto('/products');
await page.click('[data-testid="product-1"] button');
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
});
});
TDD — Test Driven Development
TDD — подход, при котором тесты пишутся до кода:
- Red — напишите падающий тест
- Green — напишите минимальный код для прохождения
- Refactor — улучшите код, сохраняя тесты зелёными
// 1. Red — пишем тест
it('should calculate discount', () => {
expect(calculateDiscount(100, 10)).toBe(90);
});
// 2. Green — минимальная реализация
function calculateDiscount(price: number, percent: number): number {
return price - (price * percent / 100);
}
// 3. Refactor — улучшаем
function calculateDiscount(price: number, percent: number): number {
if (percent < 0 || percent > 100) {
throw new Error('Invalid discount percent');
}
return price * (1 - percent / 100);
}
Покрытие кода
# Запуск тестов с покрытием
npm test -- --coverage
# Результат
--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 85.71 | 83.33 | 90.00 | 85.71 |
validation.ts | 100 | 100 | 100 | 100 |
utils.ts | 75.00 | 66.67 | 80.00 | 75.00 |
--------------------|---------|----------|---------|---------|
Заключение
Тестирование — инвестиция в качество и скорость разработки. Начните с unit тестов критичной бизнес-логики, добавьте e2e для ключевых сценариев. Автоматизируйте запуск тестов в CI/CD.
Код без тестов — это legacy код с первого дня. Пишите тесты, которые дают уверенность в рефакторинге.
