Что такое пирамида тестирования?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Пирамида тестирования (Testing Pyramid)
Пирамида тестирования — это концептуальная модель, которая показывает, как распределять тесты по типам для оптимального баланса скорости, надёжности и стоимости разработки.
Визуальная структура
/\
/ \ E2E (End-to-End) Тесты
/----\ ~5-10% всех тестов
/ \ Медленные, но проверяют весь flow
/--------\
/ \ Integration Тесты
/ \ ~20-30% всех тестов
/ \ Средняя скорость, проверяют взаимодействие
/________________\
Unit Тесты
~60-70% всех тестов
Быстрые, проверяют одну функцию
Уровни пирамиды
1. Unit Тесты (Основание)
Это быстрые тесты отдельных функций и методов в изоляции.
import pytest
from calculator import add, multiply
class TestCalculator:
def test_add_positive_numbers(self):
assert add(2, 3) == 5
def test_add_negative_numbers(self):
assert add(-2, -3) == -5
def test_multiply_zero(self):
assert multiply(5, 0) == 0
def test_multiply_negative(self):
assert multiply(-2, 3) == -6
Характеристики:
- Скорость: очень быстрые (миллисекунды)
- Изоляция: тестируют одну функцию
- Mocks: используют mock объекты для зависимостей
- Количество: большинство тестов (60-70%)
- Цена: дешевые в разработке
# Unit тест с mock
from unittest.mock import Mock, patch
def test_send_email_success():
# Mock внешнего сервиса
with patch('email_service.send') as mock_send:
mock_send.return_value = {"status": "sent"}
result = send_notification("user@example.com", "Hello")
assert result == {"status": "sent"}
mock_send.assert_called_once_with("user@example.com", "Hello")
2. Integration Тесты (Средина)
Это тесты взаимодействия между компонентами.
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def db():
engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)
return Session()
def test_create_user_and_save_to_db(db):
# Тестируем взаимодействие: код + БД
user = User(name="John", email="john@example.com")
db.add(user)
db.commit()
# Проверяем, что данные действительно в БД
saved_user = db.query(User).filter_by(email="john@example.com").first()
assert saved_user.name == "John"
def test_order_with_payment_service():
# Тестируем взаимодействие между Order и Payment
order = Order(items=[Item(price=100)], user_id=1)
# Используем реальный payment service (или почти реальный)
payment = process_payment(order) # Реальное взаимодействие
assert payment.status == "success"
assert order.paid == True
Характеристики:
- Скорость: медленнее (секунды)
- Изоляция: тестируют взаимодействие 2+ компонентов
- Mocks: минимум mock, больше реальных компонентов
- Количество: среднее (20-30%)
- Цена: дороже в разработке и поддержке
# Integration тест с реальной БД
import pytest
from fastapi.testclient import TestClient
from app import app, database
@pytest.fixture
def client():
# Использует реальную (в памяти) БД
database.reset()
return TestClient(app)
def test_create_user_through_api(client):
# Тестируем: API endpoint → SQL → БД
response = client.post(
"/api/v1/users",
json={"name": "John", "email": "john@example.com"}
)
assert response.status_code == 200
assert response.json()["name"] == "John"
# Проверяем, что данные действительно в БД
user = database.get_user_by_email("john@example.com")
assert user is not None
3. E2E Тесты (Вершина)
Это тесты полного flow приложения, как пользователь его использует.
from playwright.sync_api import sync_playwright
import pytest
@pytest.fixture
def browser():
with sync_playwright() as p:
yield p.chromium.launch()
def test_user_registration_e2e(browser):
# Открываем браузер и тестируем как реальный пользователь
page = browser.new_page()
# Переходим на страницу
page.goto("http://localhost:3000/register")
# Заполняем форму
page.fill('input[name="name"]', "John Doe")
page.fill('input[name="email"]', "john@example.com")
page.fill('input[name="password"]', "password123")
# Отправляем форму
page.click('button[type="submit"]')
# Проверяем редирект на страницу входа
page.wait_for_url("http://localhost:3000/login")
assert page.url == "http://localhost:3000/login"
# Логинимся
page.fill('input[name="email"]', "john@example.com")
page.fill('input[name="password"]', "password123")
page.click('button[type="submit"]')
# Проверяем, что мы на домашней странице
page.wait_for_url("http://localhost:3000/dashboard")
assert "Dashboard" in page.text_content()
page.close()
Характеристики:
- Скорость: очень медленные (секунды-минуты)
- Изоляция: тестируют весь flow (backend + frontend + БД)
- Mocks: нет mock, всё реально
- Количество: мало (5-10%)
- Цена: очень дорогие (медленные, хрупкие, долгие в разработке)
Почему пирамида?
ПОЧЕМУ именно пирамида, а не перевёрнутая пирамида?
Пирамида (правильная структура):
┌─────────────────────┐
│ E2E Тесты (5%) │ Медленные
├───────────────────┐│
│ Integration (25%) ││ Средние
├─────────────────┐││
│ Unit Тесты(70%)│││ Быстрые
└─────────────────┴┴┴
Перевёрнутая пирамида (ПЛОХО):
┌─────┐
│ E2E │ Много медленных тестов
├──────┤
│ Integ│
├────────┐
│ Unit │ Мало быстрых тестов
└────────┘
Проблемы перевёрнутой:
1. E2E тесты медленные (30 мин на запуск)
2. Разработчикам нужен feedback за 30 сек
3. Много хрупких тестов (падают при мелких изменениях)
4. Дорого разрабатывать и поддерживать
Практический пример: полная стратегия
# Модуль: сервис заказов (Order Service)
# ===== UNIT ТЕСТЫ (60-70%) =====
class TestOrderCalculation:
"""Тестируем расчёты в изоляции"""
def test_calculate_subtotal(self):
items = [Item(price=10), Item(price=20)]
assert calculate_subtotal(items) == 30
def test_apply_discount(self):
total = 100
assert apply_discount(total, discount_percent=10) == 90
def test_calculate_tax(self):
subtotal = 100
assert calculate_tax(subtotal) == 15 # 15% tax
def test_calculate_final_price(self):
order_data = {"items": [Item(10), Item(20)], "discount": 10}
# subtotal = 30
# tax = 30 * 0.15 = 4.5
# discount = 30 * 0.1 = 3
# final = 30 + 4.5 - 3 = 31.5
assert calculate_final_price(order_data) == 31.5
# ===== INTEGRATION ТЕСТЫ (20-30%) =====
class TestOrderService:
"""Тестируем взаимодействие Order + БД + Payment"""
def test_create_order_saves_to_database(self, db):
service = OrderService(db)
order = service.create_order(
user_id=1,
items=[Item(id=1, price=10)]
)
# Проверяем в БД
saved = db.query(Order).get(order.id)
assert saved.user_id == 1
assert saved.total_price == 10
def test_order_triggers_payment(self, db, payment_mock):
service = OrderService(db)
order = service.create_order(user_id=1, items=[...])
# Проверяем, что был вызван payment service
payment_mock.assert_called_once()
assert order.payment_status == "pending"
def test_cancel_order_refunds_payment(self, db):
service = OrderService(db)
order = service.create_order(...)
service.cancel_order(order.id)
# Проверяем, что статус изменился
refunded_order = db.query(Order).get(order.id)
assert refunded_order.status == "cancelled"
assert refunded_order.refund_status == "processed"
# ===== E2E ТЕСТЫ (5-10%) =====
class TestOrderFlowE2E:
"""Тестируем весь flow: от выбора товара до подтверждения"""
def test_complete_order_flow(self, browser):
# 1. Пользователь заходит в магазин
page = browser.new_page()
page.goto("http://localhost:3000/products")
# 2. Выбирает товар
page.click('button:has-text("Add to cart")', nth=0)
page.click('button:has-text("Add to cart")', nth=1)
# 3. Переходит в корзину
page.click('a:has-text("Cart")')
assert page.locator('.cart-item').count() == 2
# 4. Оформляет заказ
page.click('button:has-text("Checkout")')
# 5. Вводит данные
page.fill('input[name="email"]', "user@example.com")
page.fill('input[name="card"]', "4111111111111111")
# 6. Подтверждает
page.click('button:has-text("Confirm")')
# 7. Проверяет успешность
page.wait_for_url("**/order-confirmation*")
assert "Order confirmed" in page.text_content()
page.close()
Как писать тесты по пирамиде
Правило 1: Начинай с Unit тестов
# Шаг 1: Unit тесты функций
def test_validate_email():
assert validate_email("user@example.com") == True
assert validate_email("invalid") == False
# Шаг 2: Integration тесты
def test_user_creation_with_email_validation():
user = create_user("John", "john@example.com")
assert user.email == "john@example.com"
# Шаг 3: E2E тест
def test_user_can_register_and_login():
# Полный flow
pass
Правило 2: Для каждого Unit теста думай об Integration
# Если написал unit тест для функции
def test_create_order_calculates_total():
# Unit тест
pass
# Подумай: а что если функция вызывает БД?
def test_create_order_saves_to_database():
# Integration тест
pass
# Подумай: а что если функция вызывает внешний API?
def test_create_order_calls_payment_service():
# Integration тест
pass
Правило 3: E2E только для критичных flow'ов
# Напиши E2E для:
✓ Регистрация пользователя
✓ Оформление заказа
✓ Оплата платежа
✓ Восстановление пароля
# НЕ напиши E2E для:
✗ Сортировка товаров
✗ Фильтрация по цене
✗ Отображение баннера
Лучшие практики
1. Тестовое покрытие должно быть 80-90%
# Проверь покрытие
pytest --cov=app --cov-report=html
# Открой report.html в браузере
# Зелёное = покрыто, красное = не покрыто
2. Unit тесты должны выполняться < 1 сек
pytest tests/unit/ # Должно выполниться за 100ms
3. Integration тесты < 10 сек
pytest tests/integration/ # Должно выполниться за 5 сек
4. E2E тесты < 1 мин
pytest tests/e2e/ # Должно выполниться за 30 сек
Резюме
Пирамида тестирования:
- Unit Тесты (70%) — быстрые, дешевые, для отдельных функций
- Integration Тесты (25%) — взаимодействие компонентов
- E2E Тесты (5%) — полный flow приложения
Почему пирамида:
- Юнит тесты быстрые = хороший feedback
- Integration тесты средние = баланс скорости и покрытия
- E2E тесты медленные = только для критичных сценариев
Главное правило: 70% tests за < 1 сек, 25% за < 10 сек, 5% за < 1 мин.