architect/_archive/2025-11-cleanup/templates/TESTING.template.md

🧪 TESTING - Стратегия тестирования

Проект: {PROJECT_NAME}
Последнее обновление: {ДАТА}


📑 СОДЕРЖАНИЕ

  1. Стратегия тестирования
  2. Unit тесты
  3. Integration тесты
  4. E2E тесты
  5. Performance тесты
  6. Security тесты
  7. Запуск тестов
  8. CI/CD интеграция
  9. Coverage требования

🎯 СТРАТЕГИЯ ТЕСТИРОВАНИЯ {#стратегия-тестирования}

Пирамида тестирования

         /\
        /  \  E2E Tests (10%)
       /----\
      /      \  Integration Tests (30%)
     /--------\
    /          \  Unit Tests (60%)
   /____________\

Принципы

  1. Unit тесты - быстрые и много
    - Покрытие бизнес-логики
    - Тестируем функции изолированно
    - Моки для внешних зависимостей

  2. Integration тесты - средние
    - Проверяем взаимодействие компонентов
    - Тестируем API endpoints
    - Реальная БД (тестовая)

  3. E2E тесты - медленные и критичные
    - Проверяем полные сценарии пользователя
    - Браузерные тесты (Playwright/Selenium)
    - Только критичные flow


🔬 UNIT ТЕСТЫ {#unit-тесты}

Что тестируем

Структура

tests/
├── unit/
│   ├── test_models.py
│   ├── test_utils.py
│   ├── test_validators.py
│   └── test_business_logic.py

Пример (Python/pytest)

# 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

🔗 INTEGRATION ТЕСТЫ {#integration-тесты}

Что тестируем

Структура

tests/
├── integration/
│   ├── conftest.py          # Fixtures (test DB, test client)
│   ├── test_api_auth.py
│   ├── test_api_orders.py
│   ├── test_database.py
│   └── test_ozon_integration.py

Пример (FastAPI)

# 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()

Fixtures (pytest)

# 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

🌐 E2E ТЕСТЫ {#e2e-тесты}

Что тестируем

Технологии

Структура

tests/
├── e2e/
   ├── test_user_registration.spec.js
   ├── test_create_order_flow.spec.js
   └── test_marketplace_integration.spec.js

Пример (Playwright)

// 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

⚡ PERFORMANCE ТЕСТЫ {#performance-тесты}

Load Testing (Locust)

# 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

Бенчмарки (pytest-benchmark)

def test_order_calculation_performance(benchmark):
    """Проверка производительности расчёта заказа"""
    result = benchmark(calculate_complex_order, large_order_data)
    assert result is not None
    # Benchmark автоматически измеряет время

🔒 SECURITY ТЕСТЫ {#security-тесты}

SQL Injection

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  # Таблица существует

XSS Protection

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 "&lt;script&gt;" in comment.json()["text"]

Authentication

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

🔄 CI/CD ИНТЕГРАЦИЯ {#cicd-интеграция}

GitHub Actions

# .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

Pre-commit hook

# .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"

📊 COVERAGE ТРЕБОВАНИЯ {#coverage-требования}

Минимальные требования

Проверка coverage

# Генерация отчёта
pytest --cov=modules --cov-report=html

# Открыть отчёт
open htmlcov/index.html

# Fail если меньше 70%
pytest --cov=modules --cov-fail-under=70

В CI/CD

- name: Check coverage
  run: |
    pytest --cov=modules --cov-report=term --cov-fail-under=70

📋 CHECKLIST ПЕРЕД РЕЛИЗОМ


Последнее обновление: {ДАТА}
Автор: {АВТОР}