| name | Testing & Quality Assurance |
| description | Автоматизация тестирования и проверки качества кода |
| version | 2.0.0 |
| author | Family Budget Team |
| tags | ["testing","pytest","quality","coverage","linting","shared-budget"] |
| dependencies | ["api-development"] |
Testing & Quality Assurance Skill
Автоматизация создания тестов и проверки качества кода для проекта Family Budget.
Когда использовать этот скил
Используй этот скил когда нужно:
- Создать unit тесты для endpoint/модели
- Создать integration тесты для workflow
- Создать e2e тесты для user journey
- Запустить тесты с coverage
- Проверить качество кода (linting, formatting, type checking)
- Создать тесты для Telegram bot handlers
Скил автоматически вызывается при запросах типа:
- "Создай тесты для endpoint X"
- "Добавь unit тесты для модели Y"
- "Запусти все тесты с coverage"
- "Проверь качество кода"
Контекст проекта
Проект использует:
- pytest 7.4+ для тестирования
- pytest-asyncio для async тестов
- httpx.AsyncClient для тестирования API
- pytest-cov для coverage отчетов
- ruff 0.1+ для linting
- black 23.11+ для formatting
- mypy 1.7+ для type checking
- Shared Family Budget модель - тесты БЕЗ user_id фильтрации
Структура тестов
backend/tests/
├── unit/ # Unit тесты (изолированные)
│ ├── models/ # Тесты моделей
│ ├── services/ # Тесты сервисов (SCD2, JWT, etc.)
│ └── core/ # Тесты core модулей
├── integration/ # Integration тесты (с БД)
│ ├── test_auth_flow.py
│ ├── test_article_hierarchy.py
│ ├── test_scd_type2_versioning.py
│ └── test_user_isolation.py
├── e2e/ # End-to-end тесты
│ ├── test_user_journey.py
│ └── test_admin_journey.py
├── endpoints/ # API endpoint тесты
│ ├── test_auth.py
│ ├── test_articles.py
│ ├── test_facts.py
│ └── test_users.py
└── conftest.py # Fixtures и setup
bot/tests/
├── test_start_handler.py
├── test_add_handler.py
├── test_summary_handler.py
└── conftest.py
Шаблон Unit теста для Endpoint
Создавай unit тесты для каждого endpoint со следующей структурой:
"""
Unit tests for {ModelName} endpoints.
Tests:
- CRUD operations (create, read, update, delete, list)
- User isolation
- SCD Type 2 versioning (for dimension tables)
- Error handling (404, 403, 401, 422)
- Input validation
"""
import pytest
from httpx import AsyncClient
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.app.models.user import User
from backend.app.models.{model_name} import {ModelName}
@pytest.mark.asyncio
async def test_create_{model_name}_success(
client: AsyncClient,
test_user_token: str,
test_user: User,
):
"""Test creating a {model_name} successfully."""
payload = {
"name": "Test {ModelName}",
"description": "Test description",
}
response = await client.post(
"/api/v1/{model_name}s",
json=payload,
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test {ModelName}"
assert data["user_id"] == test_user.id
assert data["is_current"] is True
assert "id" in data
assert "created_at" in data
{model_name}_unauthorized(client: AsyncClient):
payload = {: }
response = client.post(
,
json=payload,
)
response.status_code ==
{model_name}_validation_error(
client: AsyncClient,
test_user_token: ,
):
payload = {
: ,
}
response = client.post(
,
json=payload,
headers={: },
)
response.status_code ==
{model_name}_success(
client: AsyncClient,
test_user_token: ,
test_{model_name}: {ModelName},
):
response = client.get(
,
headers={: },
)
response.status_code ==
data = response.json()
data[] == test_{model_name}.
data[] == test_{model_name}.name
{model_name}_not_found(
client: AsyncClient,
test_user_token: ,
):
response = client.get(
,
headers={: },
)
response.status_code ==
{model_name}_creates_new_version(
client: AsyncClient,
test_user_token: ,
test_{model_name}: {ModelName},
session: AsyncSession,
):
old_id = test_{model_name}.
old_name = test_{model_name}.name
payload = {: }
response = client.put(
,
json=payload,
headers={: },
)
response.status_code ==
data = response.json()
data[] != old_id
data[] ==
data[]
session.refresh(test_{model_name})
test_{model_name}. == old_id
test_{model_name}.name == old_name
test_{model_name}.is_current
test_{model_name}.valid_to.year !=
{model_name}_soft_delete(
client: AsyncClient,
test_user_token: ,
test_{model_name}: {ModelName},
session: AsyncSession,
):
response = client.delete(
,
headers={: },
)
response.status_code ==
session.refresh(test_{model_name})
test_{model_name}.is_current
{model_name}s_pagination(
client: AsyncClient,
test_user_token: ,
):
response = client.get(
,
headers={: },
)
response.status_code ==
data = response.json()
data
data
data
data
(data[], )
{model_name}(
client: AsyncClient,
test_user_token: ,
other_user_{model_name}: {ModelName},
):
response = client.get(
,
headers={: },
)
response.status_code ==
data = response.json()
data[] == other_user_{model_name}.
Шаблон Integration теста
Для тестирования сложных workflow используй integration тесты:
"""
Integration test for {workflow_name}.
Tests complete workflow end-to-end with real database.
"""
import pytest
from httpx import AsyncClient
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.app.models.user import User
from backend.app.models.article import Article
from backend.app.models.fact import BudgetFact
@pytest.mark.asyncio
async def test_{workflow_name}_complete_flow(
client: AsyncClient,
session: AsyncSession,
test_user_token: str,
test_user: User,
):
"""
Test complete {workflow_name} workflow.
Steps:
1. Create article
2. Create fact with article
3. Update fact
4. Verify SCD Type 2 versioning
5. Delete fact
6. Verify soft delete
"""
article_payload = {
"name": "Food",
"type": "expense",
"description": "Food expenses"
}
article_response = await client.post(
"/api/v1/articles",
json=article_payload,
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert article_response.status_code == 201
article = article_response.json()
fact_payload = {
"article_id": article["id"],
"amount": 100.50,
"date": ,
: ,
}
fact_response = client.post(
,
json=fact_payload,
headers={: },
)
fact_response.status_code ==
fact = fact_response.json()
update_payload = {: }
update_response = client.put(
,
json=update_payload,
headers={: },
)
update_response.status_code ==
updated_fact = update_response.json()
updated_fact[] ==
updated_fact[] == fact[]
delete_response = client.delete(
,
headers={: },
)
delete_response.status_code ==
get_response = client.get(
,
headers={: },
)
get_response.status_code ==
Fixtures в conftest.py
Создай reusable fixtures для тестов:
"""
Pytest fixtures for tests.
Provides:
- Database session
- Authenticated test client
- Test users with JWT tokens
- Test models (articles, facts, etc.)
"""
import asyncio
import pytest
from typing import AsyncGenerator
from httpx import AsyncClient
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from backend.app.db.session import engine, get_async_session
from backend.app.main import app
from backend.app.models.user import User
from backend.app.models.article import Article
from backend.app.services.jwt import create_access_token
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for async tests."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="function")
async def session() -> AsyncGenerator[AsyncSession, None]:
"""Create test database session."""
async with AsyncSession(engine) as session:
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
session
engine.begin() conn:
conn.run_sync(SQLModel.metadata.drop_all)
() -> AsyncGenerator[AsyncClient, ]:
():
session
app.dependency_overrides[get_async_session] = override_get_session
AsyncClient(app=app, base_url=) client:
client
app.dependency_overrides.clear()
() -> User:
user = User(
telegram_id=,
username=,
first_name=,
last_name=,
is_admin=,
is_active=,
)
session.add(user)
session.commit()
session.refresh(user)
user
() -> :
create_access_token(user_id=test_user.)
() -> User:
user = User(
telegram_id=,
username=,
first_name=,
last_name=,
is_admin=,
is_active=,
)
session.add(user)
session.commit()
session.refresh(user)
user
() -> :
create_access_token(user_id=admin_user.)
() -> Article:
article = Article(
user_id=test_user.,
name=,
=,
description=,
is_global=,
)
session.add(article)
session.commit()
session.refresh(article)
article
{model_name}(session: AsyncSession) -> {ModelName}:
other_user = User(
telegram_id=,
username=,
first_name=,
last_name=,
)
session.add(other_user)
session.flush()
{model_name_lower} = {ModelName}(
user_id=other_user.,
name=,
)
session.add({model_name_lower})
session.commit()
session.refresh({model_name_lower})
{model_name_lower}
Команды для тестирования
Запуск всех тестов
cd backend
pytest
cd bot
pytest
Запуск конкретного типа тестов
pytest tests/unit
pytest tests/integration
pytest tests/e2e
pytest tests/endpoints/test_articles.py
pytest tests/endpoints/test_articles.py::test_create_article_success
Coverage отчет
pytest --cov=backend --cov-report=html
open htmlcov/index.html
pytest --cov=backend --cov-report=term-missing
Code Quality проверки
ruff check backend/
ruff check --fix backend/
black backend/
black --check backend/
mypy backend/
ruff check backend/ && black --check backend/ && mypy backend/
Проверочный чеклист
После создания тестов проверь:
Связанные скилы
- api-development: для тестирования созданных endpoints
- db-management: для тестирования миграций
- bot-development: для тестирования bot handlers
Примеры использования
Пример 1: Создать тесты для endpoint
Создай unit тесты для endpoint /api/v1/articles.
Покрой все CRUD операции, user isolation, SCD Type 2 versioning.
Пример 2: Запустить тесты с coverage
Запусти все backend тесты с coverage отчетом.
Покажи файлы с coverage < 80%.
Пример 3: Проверить quality
Проверь качество кода в backend/:
- Запусти ruff linting
- Проверь formatting (black)
- Запусти type checking (mypy)
Исправь найденные проблемы.
Часто задаваемые вопросы
Q: Как мокировать API client в bot тестах?
A: Используй pytest-mock или unittest.mock:
@pytest.mark.asyncio
async def test_bot_handler(mocker):
mock_api = mocker.patch("bot.utils.api_client.get_api_client")
mock_api.return_value.list_articles.return_value = {"articles": [...]}
Q: Как тестировать SCD Type 2?
A: Проверяй что:
- UPDATE создает новую запись с новым ID
- Старая запись имеет is_current=False
- Старая запись имеет valid_to != 9999-12-31
- Новая запись имеет is_current=True
Q: Нужно ли тестировать каждый endpoint?
A: Да! Минимум: success case, 401 unauthorized, 404 not found, user isolation.