← Назад к вопросам

Что такое пирамида тестирования?

1.0 Junior🔥 181 комментариев
#Архитектура и паттерны#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Пирамида тестирования (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 мин.

Что такое пирамида тестирования? | PrepBro