architect/_archive/2025-11-26-cleanup/cifra/domains/03_testing/docs/guide.md

TEST MODE — Режим тестирования

Версия: 1.0
Дата создания: 2025-11-10


ЧТО ЭТО?

Режим для систематического тестирования приложений.

Когда использовать:
- Написание тестов для нового кода
- Запуск тестов перед commit
- Проверка coverage
- Regression тестирование
- Создание библиотеки переиспользуемых тестов


ФИЛОСОФИЯ

Проблема:

Классический подход:

User: Напиши тест для функции get_users()

Claude: Генерирует тест с нуля (2000 tokens)

Проблемы:
- Каждый раз генерация с нуля
- Нет переиспользования
- Нет стандартизации
- Трата токенов


Решение: Библиотека тестов

Новый подход:

1. Определить ТИП задачи (test_api_endpoint, test_database_model, test_streamlit_page)
2. Найти ГОТОВЫЙ тест в tests/library/
3. СКОПИРОВАТЬ и АДАПТИРОВАТЬ
4. Экономия: 80-90% токенов

Аналог:
- Как registry/ для библиотек кода
- Как templates/ для проектов
- Как components/ для UI элементов


СТРУКТУРА БИБЛИОТЕКИ ТЕСТОВ

tests/
├── catalog.yaml                    # Каталог всех тестов

├── library/                        # Библиотека переиспользуемых тестов
   ├── unit/                       # Unit тесты
      ├── test_function.py        # Тест чистой функции
      ├── test_class.py           # Тест класса
      └── test_validation.py      # Тест валидации
   
   ├── integration/                # Интеграционные
      ├── test_database.py        # Тест БД операций
      ├── test_api_endpoint.py    # Тест API endpoint
      └── test_external_api.py    # Тест внешнего API
   
   ├── e2e/                        # End-to-end
      ├── streamlit/
         ├── test_page_load.js   # Playwright
         ├── test_form_submit.js
         ├── test_navigation.js
         └── test_table_render.js
      
      └── web/
          ├── test_user_flow.js
          └── test_checkout.js
   
   └── performance/                # Нагрузочные
       ├── test_load.py            # locust
       └── test_stress.py

└── fixtures/                       # Общие fixtures
    ├── database.py                 # БД fixtures
    ├── auth.py                     # Auth fixtures
    └── mock_data.py                # Тестовые данные

ПРОЦЕДУРЫ

ПРОЦЕДУРА: Написать unit тест

Задача: Протестировать функцию calculate_total(items)

Шаги:

1. Найти подходящий шаблон:

cat tests/catalog.yaml | grep -A 10 "test_function"

2. Скопировать шаблон:

cp tests/library/unit/test_function.py tests/project/test_calculate_total.py

3. Адаптировать:

Было (шаблон):

# tests/library/unit/test_function.py
"""
Шаблон unit теста для чистой функции.

Замени:
- {{FUNCTION_NAME}} → имя функции
- {{MODULE_PATH}} → путь к модулю
- {{TEST_CASES}} → тестовые случаи
"""
import pytest
from {{MODULE_PATH}} import {{FUNCTION_NAME}}

class Test{{FUNCTION_NAME}}:
    """Тесты для {{FUNCTION_NAME}}"""

    def test_{{FUNCTION_NAME}}_happy_path(self):
        """Тест обычного случая"""
        # Arrange
        input_data = {{SAMPLE_INPUT}}

        # Act
        result = {{FUNCTION_NAME}}(input_data)

        # Assert
        assert result == {{EXPECTED_OUTPUT}}

    def test_{{FUNCTION_NAME}}_edge_case(self):
        """Тест граничного случая"""
        input_data = {{EDGE_CASE_INPUT}}
        result = {{FUNCTION_NAME}}(input_data)
        assert result == {{EDGE_CASE_OUTPUT}}

    def test_{{FUNCTION_NAME}}_error_handling(self):
        """Тест обработки ошибок"""
        with pytest.raises(ValueError):
            {{FUNCTION_NAME}}({{INVALID_INPUT}})

    @pytest.mark.parametrize("input_data,expected", [
        ({{CASE1_INPUT}}, {{CASE1_OUTPUT}}),
        ({{CASE2_INPUT}}, {{CASE2_OUTPUT}}),
        ({{CASE3_INPUT}}, {{CASE3_OUTPUT}}),
    ])
    def test_{{FUNCTION_NAME}}_parametrized(self, input_data, expected):
        """Параметризованные тесты"""
        assert {{FUNCTION_NAME}}(input_data) == expected

Стало (адаптированный):

# tests/project/test_calculate_total.py
"""
Тесты для функции calculate_total
"""
import pytest
from utils.pricing import calculate_total

class TestCalculateTotal:
    """Тесты для calculate_total"""

    def test_calculate_total_happy_path(self):
        """Тест обычного случая"""
        # Arrange
        items = [
            {"price": 100, "quantity": 2},
            {"price": 50, "quantity": 1},
        ]

        # Act
        result = calculate_total(items)

        # Assert
        assert result == 250  # 100*2 + 50*1

    def test_calculate_total_empty_list(self):
        """Тест пустого списка"""
        items = []
        result = calculate_total(items)
        assert result == 0

    def test_calculate_total_error_handling(self):
        """Тест обработки ошибок"""
        with pytest.raises(ValueError):
            calculate_total(None)

    @pytest.mark.parametrize("items,expected", [
        ([{"price": 10, "quantity": 1}], 10),
        ([{"price": 10, "quantity": 2}], 20),
        ([{"price": 10, "quantity": 0}], 0),
    ])
    def test_calculate_total_parametrized(self, items, expected):
        """Параметризованные тесты"""
        assert calculate_total(items) == expected

4. Запустить:

pytest tests/project/test_calculate_total.py -v

5. Проверить coverage:

pytest tests/project/test_calculate_total.py --cov=utils.pricing --cov-report=term

Экономия:
- Генерация с нуля: ~2000 tokens
- Адаптация шаблона: ~300 tokens
- Экономия: 85%


ПРОЦЕДУРА: Написать E2E тест (Streamlit)

Задача: Протестировать страницу Orders

Шаги:

1. Найти шаблон:

cat tests/catalog.yaml | grep -A 5 "test_streamlit_page"

2. Скопировать:

cp tests/library/e2e/streamlit/test_page_load.js tests/project/e2e/test_orders_page.js

3. Адаптировать:

Было (шаблон):

// tests/library/e2e/streamlit/test_page_load.js
/**
 * Шаблон E2E теста для Streamlit страницы (Playwright)
 *
 * Замени:
 * - {{BASE_URL}} → URL приложения
 * - {{PAGE_PATH}} → путь к странице
 * - {{PAGE_TITLE}} → ожидаемый заголовок
 * - {{KEY_ELEMENTS}} → ключевые элементы
 */
const { test, expect } = require('@playwright/test');

test.describe('{{PAGE_NAME}} Page', () => {
  test('loads successfully', async ({ page }) => {
    // Navigate
    await page.goto('{{BASE_URL}}/{{PAGE_PATH}}');

    // Wait for Streamlit to load
    await page.waitForSelector('[data-testid="stApp"]');

    // Check title
    await expect(page.locator('h1')).toContainText('{{PAGE_TITLE}}');

    // Check key elements present
    await expect(page.locator('{{SELECTOR1}}')).toBeVisible();
    await expect(page.locator('{{SELECTOR2}}')).toBeVisible();
  });

  test('displays data correctly', async ({ page }) => {
    await page.goto('{{BASE_URL}}/{{PAGE_PATH}}');
    await page.waitForSelector('[data-testid="stDataFrame"]');

    // Check table has data
    const rows = await page.locator('[data-testid="stDataFrame"] tr').count();
    expect(rows).toBeGreaterThan(0);
  });
});

Стало (адаптированный):

// tests/project/e2e/test_orders_page.js
const { test, expect } = require('@playwright/test');

const BASE_URL = 'http://localhost:8501';

test.describe('Orders Page', () => {
  test('loads successfully', async ({ page }) => {
    // Navigate
    await page.goto(`${BASE_URL}/Orders`);

    // Wait for Streamlit to load
    await page.waitForSelector('[data-testid="stApp"]');

    // Check title
    await expect(page.locator('h1')).toContainText('Заказы');

    // Check filters present
    await expect(page.locator('text=Фильтры')).toBeVisible();
    await expect(page.locator('text=Статус')).toBeVisible();
  });

  test('displays orders table', async ({ page }) => {
    await page.goto(`${BASE_URL}/Orders`);
    await page.waitForSelector('[data-testid="stDataFrame"]');

    // Check table has data
    const rows = await page.locator('[data-testid="stDataFrame"] tr').count();
    expect(rows).toBeGreaterThan(1); // Header + at least 1 row
  });

  test('filters work correctly', async ({ page }) => {
    await page.goto(`${BASE_URL}/Orders`);

    // Select filter
    await page.selectOption('select[aria-label="Статус"]', 'completed');

    // Wait for update
    await page.waitForTimeout(1000);

    // Check filtered results
    const tableText = await page.locator('[data-testid="stDataFrame"]').textContent();
    expect(tableText).toContain('completed');
  });

  test('export button works', async ({ page }) => {
    await page.goto(`${BASE_URL}/Orders`);

    // Click export
    const downloadPromise = page.waitForEvent('download');
    await page.click('button:has-text("Экспорт в Excel")');
    const download = await downloadPromise;

    // Check file downloaded
    expect(download.suggestedFilename()).toContain('.xlsx');
  });
});

4. Запустить:

npx playwright test tests/project/e2e/test_orders_page.js

ПРОЦЕДУРА: Создать fixture

Задача: Создать fixture для БД с тестовыми данными

Шаги:

1. Скопировать шаблон:

cp tests/fixtures/database.py tests/project/conftest.py

2. Адаптировать:

# tests/project/conftest.py
"""
Fixtures для тестов проекта
"""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database.models import Base, User, Order

@pytest.fixture(scope="function")
def db_session():
    """
    Создаёт временную БД для каждого теста.
    После теста — удаляет.
    """
    # Create in-memory SQLite
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)

    # Create session
    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    # Cleanup
    session.close()
    engine.dispose()

@pytest.fixture
def sample_user(db_session):
    """Создаёт тестового пользователя"""
    user = User(
        email="test@example.com",
        password_hash="hashed_password",
        role="user"
    )
    db_session.add(user)
    db_session.commit()
    return user

@pytest.fixture
def sample_orders(db_session, sample_user):
    """Создаёт тестовые заказы"""
    orders = [
        Order(user_id=sample_user.id, status="completed", total=100),
        Order(user_id=sample_user.id, status="processing", total=200),
        Order(user_id=sample_user.id, status="cancelled", total=50),
    ]
    db_session.add_all(orders)
    db_session.commit()
    return orders

3. Использовать в тестах:

# tests/project/test_order_repository.py
from repositories.order_repository import OrderRepository

def test_get_orders_by_status(db_session, sample_orders):
    """Тест фильтрации заказов по статусу"""
    # Arrange
    repo = OrderRepository(db_session)

    # Act
    completed_orders = repo.get_by_status("completed")

    # Assert
    assert len(completed_orders) == 1
    assert completed_orders[0].status == "completed"
    assert completed_orders[0].total == 100

КАТАЛОГ ТЕСТОВ

tests/catalog.yaml

version: 1.0
created: 2025-11-10

purpose: |
  Каталог всех переиспользуемых тестов.
  Уровень L4 в каскаде поиска.

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# UNIT TESTS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

unit:
  - id: test-function
    name: "Unit Test: Function"
    path: "library/unit/test_function.py"
    framework: pytest
    use_for:
      - "Чистые функции"
      - "Утилиты"
      - "Вычисления"
    variables:
      - FUNCTION_NAME
      - MODULE_PATH
      - TEST_CASES

  - id: test-class
    name: "Unit Test: Class"
    path: "library/unit/test_class.py"
    framework: pytest
    use_for:
      - "Классы"
      - "Service layer"
      - "Repositories"

  - id: test-validation
    name: "Unit Test: Validation"
    path: "library/unit/test_validation.py"
    framework: pytest
    use_for:
      - "Pydantic models"
      - "Form validation"
      - "Input sanitization"

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# INTEGRATION TESTS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

integration:
  - id: test-database-model
    name: "Integration: Database Model"
    path: "library/integration/test_database.py"
    framework: pytest + SQLAlchemy
    use_for:
      - "SQLAlchemy models"
      - "CRUD операции"
      - "Relationships"
    fixtures:
      - db_session
      - sample_data

  - id: test-api-endpoint
    name: "Integration: API Endpoint"
    path: "library/integration/test_api_endpoint.py"
    framework: pytest + FastAPI TestClient
    use_for:
      - "FastAPI endpoints"
      - "Request/Response"
      - "Authentication"

  - id: test-external-api
    name: "Integration: External API"
    path: "library/integration/test_external_api.py"
    framework: pytest + responses
    use_for:
      - "Интеграции с внешними API"
      - "HTTP клиенты"
      - "Mocking внешних сервисов"

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# E2E TESTS (Streamlit)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

e2e_streamlit:
  - id: test-streamlit-page-load
    name: "E2E: Streamlit Page Load"
    path: "library/e2e/streamlit/test_page_load.js"
    framework: playwright
    use_for:
      - "Проверка загрузки страницы"
      - "Наличие ключевых элементов"

  - id: test-streamlit-form-submit
    name: "E2E: Streamlit Form Submit"
    path: "library/e2e/streamlit/test_form_submit.js"
    framework: playwright
    use_for:
      - "Формы ввода"
      - "Отправка данных"
      - "Валидация"

  - id: test-streamlit-navigation
    name: "E2E: Streamlit Navigation"
    path: "library/e2e/streamlit/test_navigation.js"
    framework: playwright
    use_for:
      - "Навигация между страницами"
      - "Sidebar links"
      - "Page routing"

  - id: test-streamlit-table-render
    name: "E2E: Streamlit Table Rendering"
    path: "library/e2e/streamlit/test_table_render.js"
    framework: playwright
    use_for:
      - "Таблицы данных"
      - "Dataframes"
      - "Фильтры"

  - id: test-streamlit-auth-flow
    name: "E2E: Streamlit Auth Flow"
    path: "library/e2e/streamlit/test_auth_flow.js"
    framework: playwright
    use_for:
      - "Авторизация"
      - "Login/Logout"
      - "Session management"

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# FIXTURES
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

fixtures:
  - id: fixture-database
    name: "Fixture: Database Session"
    path: "fixtures/database.py"
    provides:
      - db_session (SQLAlchemy session)
      - sample_user
      - sample_orders

  - id: fixture-auth
    name: "Fixture: Authentication"
    path: "fixtures/auth.py"
    provides:
      - auth_headers (JWT token)
      - mock_user
      - authenticated_client

  - id: fixture-mock-data
    name: "Fixture: Mock Data"
    path: "fixtures/mock_data.py"
    provides:
      - mock_orders_data
      - mock_users_data
      - mock_products_data

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# STATISTICS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

statistics:
  total_tests: 13
  by_type:
    unit: 3
    integration: 3
    e2e: 5
    fixtures: 3

  token_savings:
    average: 80-85%
    example:
      generate_new: 2000 tokens
      adapt_template: 300 tokens
      savings: 85%

ПРАВИЛА РАБОТЫ С ТЕСТАМИ

1. ПЕРЕД написанием теста:

# Искать подходящий шаблон
cat tests/catalog.yaml | grep -A 10 "{тип_теста}"

2. КОПИРОВАТЬ, не генерировать:

# ✅ ХОРОШО
cp tests/library/unit/test_function.py tests/project/test_my_function.py

# ❌ ПЛОХО
# Генерировать тест с нуля

3. АДАПТИРОВАТЬ:

Заменить {{VARIABLES}} на реальные значения

4. ЗАПУСТИТЬ:

pytest tests/project/test_my_function.py -v

5. ДОБАВИТЬ в проект:

git add tests/project/test_my_function.py
git commit -m "test: добавлен тест для my_function"

ЗАПУСК ТЕСТОВ

Все тесты:

pytest

Конкретный файл:

pytest tests/project/test_orders.py

С coverage:

pytest --cov=solution/mvp --cov-report=html

Только E2E:

npx playwright test

Только unit:

pytest -m unit

КОНФИГУРАЦИЯ

scope: project
mode: test

loaded_files:
  - tests/catalog.yaml
  - tests/library/**
  - solution/mvp/** (для тестирования)

cascade_enabled: true
registry_enabled: true (для pytest, playwright)

actions_allowed:
  - read_code: true
  - write_tests: true
  - run_tests: true
  - generate_coverage: true

test_frameworks:
  python: pytest
  e2e: playwright
  mocking: responses, pytest-mock

journaling: true

Текущий режим: Test Mode
Фокус: Систематическое тестирование
Philosophy: Копировать и адаптировать, не генерировать