Версия: 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 # Тестовые данные
Задача: Протестировать функцию 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%
Задача: Протестировать страницу 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 для БД с тестовыми данными
Шаги:
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
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%
# Искать подходящий шаблон
cat tests/catalog.yaml | grep -A 10 "{тип_теста}"
# ✅ ХОРОШО
cp tests/library/unit/test_function.py tests/project/test_my_function.py
# ❌ ПЛОХО
# Генерировать тест с нуля
Заменить {{VARIABLES}} на реальные значения
pytest tests/project/test_my_function.py -v
git add tests/project/test_my_function.py
git commit -m "test: добавлен тест для my_function"
pytest
pytest tests/project/test_orders.py
pytest --cov=solution/mvp --cov-report=html
npx playwright test
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: Копировать и адаптировать, не генерировать