Проект: {PROJECT_NAME}
Последнее обновление: {ДАТА}
/\
/ \ E2E Tests (10%)
/----\
/ \ Integration Tests (30%)
/--------\
/ \ Unit Tests (60%)
/____________\
Unit тесты - быстрые и много
- Покрытие бизнес-логики
- Тестируем функции изолированно
- Моки для внешних зависимостей
Integration тесты - средние
- Проверяем взаимодействие компонентов
- Тестируем API endpoints
- Реальная БД (тестовая)
E2E тесты - медленные и критичные
- Проверяем полные сценарии пользователя
- Браузерные тесты (Playwright/Selenium)
- Только критичные flow
tests/
├── unit/
│ ├── test_models.py
│ ├── test_utils.py
│ ├── test_validators.py
│ └── test_business_logic.py
# tests/unit/test_order_calculator.py
import pytest
from modules.business.order_calculator import calculate_total
def test_calculate_total_with_tax():
"""Проверка расчёта с налогом"""
base_price = 100
tax = 20
result = calculate_total(base_price, tax)
assert result == 120.0
def test_calculate_total_zero_tax():
"""Проверка расчёта без налога"""
base_price = 100
tax = 0
result = calculate_total(base_price, tax)
assert result == 100.0
def test_calculate_total_negative_raises_error():
"""Проверка что негативная цена вызывает ошибку"""
with pytest.raises(ValueError):
calculate_total(-100, 20)
@pytest.mark.parametrize("base,tax,expected", [
(100, 20, 120),
(50, 10, 55),
(1000, 5, 1050),
])
def test_calculate_total_multiple_cases(base, tax, expected):
"""Параметризованный тест"""
assert calculate_total(base, tax) == expected
# Все unit тесты
pytest tests/unit/ -v
# С покрытием
pytest tests/unit/ --cov=modules --cov-report=html
# Конкретный файл
pytest tests/unit/test_order_calculator.py -v
# Конкретный тест
pytest tests/unit/test_order_calculator.py::test_calculate_total_with_tax -v
tests/
├── integration/
│ ├── conftest.py # Fixtures (test DB, test client)
│ ├── test_api_auth.py
│ ├── test_api_orders.py
│ ├── test_database.py
│ └── test_ozon_integration.py
# tests/integration/test_api_orders.py
import pytest
from fastapi.testclient import TestClient
def test_create_order_success(client, test_db, auth_headers):
"""Проверка создания заказа через API"""
order_data = {
"customer_name": "Иван Иванов",
"customer_phone": "+79991234567",
"items": [
{"sku": "SKU-001", "quantity": 2, "price": 1500}
]
}
response = client.post(
"/api/orders",
json=order_data,
headers=auth_headers
)
assert response.status_code == 201
data = response.json()
assert data["customer_name"] == "Иван Иванов"
assert data["id"] is not None
# Проверить что заказ в БД
order = test_db.query(Order).filter(Order.id == data["id"]).first()
assert order is not None
assert order.customer_name == "Иван Иванов"
def test_create_order_validation_error(client, auth_headers):
"""Проверка валидации при создании заказа"""
invalid_data = {
"customer_name": "", # Пустое имя
"items": [] # Нет товаров
}
response = client.post("/api/orders", json=invalid_data, headers=auth_headers)
assert response.status_code == 422 # Validation error
assert "validation" in response.json()["error"]["code"].lower()
# tests/integration/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def test_db():
"""Создать тестовую БД"""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
@pytest.fixture
def client(test_db):
"""Test client для API"""
from main import app
app.dependency_overrides[get_db] = lambda: test_db
return TestClient(app)
@pytest.fixture
def auth_headers():
"""Заголовки авторизации"""
token = create_test_token()
return {"Authorization": f"Bearer {token}"}
# Integration тесты
pytest tests/integration/ -v
# С маркерами
pytest -m integration -v
tests/
├── e2e/
│ ├── test_user_registration.spec.js
│ ├── test_create_order_flow.spec.js
│ └── test_marketplace_integration.spec.js
// tests/e2e/test_create_order_flow.spec.js
const { test, expect } = require('@playwright/test');
test('создание заказа - полный flow', async ({ page }) => {
// 1. Авторизация
await page.goto('http://localhost:8501');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button:has-text("Войти")');
await expect(page).toHaveURL(/.*dashboard/);
// 2. Переход на страницу заказов
await page.click('text=Заказы');
await expect(page).toHaveURL(/.*orders/);
// 3. Создание заказа
await page.click('button:has-text("Создать заказ")');
await page.fill('input[name="customer_name"]', 'Тестовый Покупатель');
await page.fill('input[name="customer_phone"]', '+79991234567');
await page.fill('textarea[name="address"]', 'Москва, ул. Тестовая, д.1');
// Добавить товар
await page.click('button:has-text("Добавить товар")');
await page.selectOption('select[name="sku"]', 'SKU-001');
await page.fill('input[name="quantity"]', '2');
// Отправить
await page.click('button[type="submit"]');
// 4. Проверка результата
await expect(page.locator('text=✅ Заказ создан')).toBeVisible();
await expect(page.locator('text=Заказ #')).toBeVisible();
// 5. Проверка что заказ в списке
await page.goto('http://localhost:8501/orders');
await expect(page.locator('text=Тестовый Покупатель')).toBeVisible();
});
test('загрузка заказов из Ozon', async ({ page }) => {
// Авторизация
await page.goto('http://localhost:8501');
await loginAsAdmin(page);
// Перейти на страницу интеграций
await page.click('text=Интеграции');
await page.click('text=Ozon');
// Загрузить заказы
await page.click('button:has-text("Загрузить заказы")');
// Выбрать период
await page.fill('input[name="date_from"]', '2025-11-01');
await page.fill('input[name="date_to"]', '2025-11-08');
await page.click('button:has-text("Загрузить")');
// Ждём загрузки
await expect(page.locator('text=Загружено заказов:')).toBeVisible({ timeout: 10000 });
// Проверка что заказы появились
await page.goto('http://localhost:8501/orders');
await expect(page.locator('.order-row').first()).toBeVisible();
});
# Все E2E тесты
npx playwright test
# С UI
npx playwright test --ui
# Headless
npx playwright test --headed
# Конкретный браузер
npx playwright test --project=chromium
# tests/performance/locustfile.py
from locust import HttpUser, task, between
class MarketplaceUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Авторизация при старте"""
self.client.post("/api/auth/login", json={
"email": "test@example.com",
"password": "password123"
})
@task(3) # Вес 3
def get_orders(self):
"""Получить список заказов"""
self.client.get("/api/orders")
@task(1) # Вес 1
def create_order(self):
"""Создать заказ"""
self.client.post("/api/orders", json={
"customer_name": "Load Test User",
"items": [{"sku": "SKU-001", "quantity": 1}]
})
Запуск:
# 100 пользователей, 10 новых/сек
locust -f tests/performance/locustfile.py --users 100 --spawn-rate 10
def test_order_calculation_performance(benchmark):
"""Проверка производительности расчёта заказа"""
result = benchmark(calculate_complex_order, large_order_data)
assert result is not None
# Benchmark автоматически измеряет время
def test_sql_injection_protection():
"""Проверка защиты от SQL injection"""
malicious_input = "'; DROP TABLE users; --"
response = client.post("/api/users", json={
"email": malicious_input
})
# Должна быть валидация, не SQL ошибка
assert response.status_code == 422
# Проверить что таблица не удалена
users = db.query(User).all()
assert len(users) >= 0 # Таблица существует
def test_xss_protection():
"""Проверка экранирования HTML"""
xss_payload = "<script>alert('XSS')</script>"
response = client.post("/api/comments", json={
"text": xss_payload
})
# Получить обратно
comment = client.get(f"/api/comments/{response.json()['id']}")
# Скрипт должен быть экранирован
assert "<script>" not in comment.json()["text"]
assert "<script>" in comment.json()["text"]
def test_unauthorized_access():
"""Проверка что без токена нельзя получить данные"""
response = client.get("/api/orders") # Без auth headers
assert response.status_code == 401
assert "unauthorized" in response.json()["error"]["code"].lower()
# Python/pytest
pytest
# Node.js/Jest
npm test
# С покрытием
pytest --cov=modules --cov-report=html
npm test -- --coverage
# Unit
pytest tests/unit/ -v
# Integration
pytest tests/integration/ -v
# E2E
npx playwright test
# В тестах
@pytest.mark.slow
def test_long_running():
pass
@pytest.mark.integration
def test_api():
pass
# Запуск по маркерам
pytest -m "not slow" # Пропустить медленные
pytest -m integration # Только integration
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: |
pytest --cov=modules --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
# .git/hooks/pre-commit
#!/bin/bash
echo "Running tests..."
pytest tests/unit/ -v
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Commit aborted."
exit 1
fi
echo "✅ Tests passed"
# Генерация отчёта
pytest --cov=modules --cov-report=html
# Открыть отчёт
open htmlcov/index.html
# Fail если меньше 70%
pytest --cov=modules --cov-fail-under=70
- name: Check coverage
run: |
pytest --cov=modules --cov-report=term --cov-fail-under=70
Последнее обновление: {ДАТА}
Автор: {АВТОР}