plugins/python/skills/pytest-patterns/SKILL.md
# Pytest Patterns Quality testing patterns with pytest for FastAPI projects. **TDD-first approach**. ## Triggers Use this skill when the user: - Wants to write tests - Asks about pytest, fixtures, mocks - Wants to set up testing for FastAPI - Uses TDD approach ## Main Principle: TDD 1. **Red** — write a failing test 2. **Green** — write minimal code 3. **Refactor** — improve while keeping tests green ## What Makes a Good Test ### 1. Clear Name ```python # ✅ Good — describes what, when, a
npx skillsauth add ruslan-korneev/claude-plugins plugins/python/skills/pytest-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quality testing patterns with pytest for FastAPI projects. TDD-first approach.
Use this skill when the user:
# ✅ Good — describes what, when, and expectation
async def test_create_user_with_valid_data_returns_user_with_id():
async def test_create_user_with_duplicate_email_raises_conflict_error():
async def test_get_user_by_id_when_not_exists_raises_not_found():
# ❌ Bad
async def test_create_user():
async def test_user():
async def test_1():
# ✅ Good — one test = one scenario
async def test_create_user_returns_user_with_id(service):
result = await service.create(user_data)
assert result.id is not None
async def test_create_user_saves_email(service):
result = await service.create(user_data)
assert result.email == user_data["email"]
# ❌ Bad — tests multiple things
async def test_create_user(service):
result = await service.create(user_data)
assert result.id is not None
assert result.email == user_data["email"]
assert result.created_at is not None
assert await service.get(result.id) == result
async def test_create_order_calculates_total(service):
# Arrange — prepare data
items = [
{"product_id": 1, "quantity": 2, "price": 100},
{"product_id": 2, "quantity": 1, "price": 50},
]
# Act — execute action
order = await service.create(items=items)
# Assert — verify result
assert order.total == 250
Tests should not depend on each other:
# ✅ Good — each test is independent
@pytest.fixture
async def session(engine):
async with engine.connect() as conn:
await conn.begin()
async with AsyncSession(bind=conn) as session:
yield session
await conn.rollback() # Rollback after each test
# ❌ Bad — shared state
_created_user = None
async def test_create_user(service):
global _created_user
_created_user = await service.create(data)
async def test_get_user(service):
user = await service.get(_created_user.id) # Depends on previous test!
# ✅ Use parametrize for similar checks
@pytest.mark.parametrize("email,is_valid", [
("[email protected]", True),
("[email protected]", True),
("invalid", False),
("@example.com", False),
("user@", False),
])
def test_email_validation(email: str, is_valid: bool):
assert validate_email(email) == is_valid
# ❌ Do NOT use parametrize for different scenarios
# Better to have separate tests with clear names
async def test_create_user_success(service):
...
async def test_create_user_duplicate_email(service):
...
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
@pytest.fixture
async def session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
@pytest.fixture
async def client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient(transport=ASGITransport(app=app)) as client:
yield client
More details: ${CLAUDE_PLUGIN_ROOT}/skills/pytest-patterns/references/fastapi-testing.md
from httpx import ASGITransport, AsyncClient
async def test_create_user_endpoint(client: AsyncClient):
response = await client.post(
"/api/v1/users",
json={"email": "[email protected]", "name": "Test"},
)
assert response.status_code == 201
assert response.json()["id"] is not None
@pytest.fixture
async def client(session: AsyncSession):
def override_session():
yield session
app.dependency_overrides[get_session] = override_session
async with AsyncClient(transport=ASGITransport(app=app)) as client:
yield client
app.dependency_overrides.clear()
More details: ${CLAUDE_PLUGIN_ROOT}/skills/pytest-patterns/references/fixtures.md
# tests/conftest.py
@pytest.fixture(scope="session")
def engine():
return create_async_engine(TEST_DATABASE_URL)
@pytest.fixture
async def session(engine) -> AsyncGenerator[AsyncSession, None]:
async with engine.connect() as conn:
await conn.begin()
session = AsyncSession(bind=conn)
yield session
await conn.rollback()
@pytest.fixture
async def client(session) -> AsyncGenerator[AsyncClient, None]:
app.dependency_overrides[get_session] = lambda: session
async with AsyncClient(transport=ASGITransport(app=app)) as c:
yield c
app.dependency_overrides.clear()
Use real database connection, not mocks. Test database is created specifically for tests.
# ✅ Good — real test database
@pytest.fixture(scope="session")
def engine():
return create_async_engine("postgresql+asyncpg://test:test@localhost:5432/test_db")
@pytest.fixture
async def session(engine) -> AsyncGenerator[AsyncSession, None]:
async with engine.connect() as conn:
await conn.begin()
async with AsyncSession(bind=conn) as session:
yield session
await conn.rollback() # Isolation via transactions
# ❌ Bad — mock database
@pytest.fixture
def mock_session():
return AsyncMock(spec=AsyncSession) # Doesn't test real SQL!
Each worker gets its own DB: gw{N}_test_{dbname}
def get_test_db_name(worker_id: str, base_name: str = "myapp") -> str:
"""gw0 → gw0_test_myapp, master → test_myapp"""
if worker_id == "master":
return f"test_{base_name}"
return f"{worker_id}_test_{base_name}"
@pytest.fixture(scope="session")
def database_url(worker_id: str) -> str:
db_name = get_test_db_name(worker_id)
return f"postgresql+asyncpg://test:test@localhost:5432/{db_name}"
More details: ${CLAUDE_PLUGIN_ROOT}/skills/pytest-patterns/references/fixtures.md
More details: ${CLAUDE_PLUGIN_ROOT}/skills/pytest-patterns/references/mocking.md
datetime.now()), random, uuidfrom unittest.mock import AsyncMock
@pytest.fixture
def mock_email_service():
"""Mock EXTERNAL email service (SendGrid, AWS SES)."""
mock = AsyncMock()
mock.send.return_value = True
return mock
@pytest.fixture
def mock_payment_gateway():
"""Mock EXTERNAL payment gateway."""
mock = AsyncMock()
mock.charge.return_value = {"status": "success", "transaction_id": "123"}
return mock
Branch coverage — one of the most valuable metrics, not just line coverage.
def process(value: int | None) -> str:
if value is not None:
return f"Value: {value}"
return "No value"
# Line coverage 100% with one test:
def test_process():
assert process(42) == "Value: 42"
# But branch `return "No value"` is NOT tested!
# Branch coverage requires both paths:
def test_process_with_value():
assert process(42) == "Value: 42"
def test_process_with_none():
assert process(None) == "No value"
# pyproject.toml
[tool.coverage.run]
source = ["src"]
branch = true # IMPORTANT: enable branch coverage
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
# Run tests with coverage
pytest --cov=src --cov-branch --cov-report=term-missing
# HTML report for detailed analysis
pytest --cov=src --cov-branch --cov-report=html
# Open htmlcov/index.html — shows uncovered branches
/test:first <feature> — create test BEFORE implementation (TDD)/test:fixture <name> — create a fixture/test:mock <dependency> — create a mocktest-reviewer — coverage and quality analysistools
# Code Review Skill This skill should be used when the user asks for "code review", "review my changes", "review this PR", "check my code", "pre-merge review", "review diff", or mentions reviewing code quality, implementation correctness, or preparing changes for merge. ## Overview Code review following the **Review Pyramid** methodology — a prioritized approach that focuses on what matters most. ## The Review Pyramid ``` ▲ /|\ 5. Code Style (Nit) ← Least important,
tools
# Architecture Patterns System-level architecture patterns for Python backend projects (FastAPI + SQLAlchemy 2.0). ## Triggers Use this skill when the user: - Designs a new system or major feature - Asks about project structure or module boundaries - Makes data modeling decisions - Designs API contracts - Evaluates architectural trade-offs - Creates or reviews Architecture Decision Records (ADR) - Needs to modernize legacy code ## Abstraction Levels | Plugin | Level | Focus | |--------|----
tools
# Python Typing Patterns Python type annotation patterns without `type: ignore`. Always the correct solution. ## Triggers Use this skill when the user: - Gets mypy/pyright errors - Asks about Python type annotations - Wants to add type hints - Works with generics, protocols, TypeVar ## Main Principle: NEVER type: ignore Every type error has a correct solution. `type: ignore` is: - Masking potential bugs - Disabling type checking - Technical debt More details: `${CLAUDE_PLUGIN_ROOT}/skills/
tools
# Ruff Patterns Knowledge about the ruff linter and patterns for solving errors. **ZERO noqa policy** — always look for the proper solution. ## Triggers Use this skill when the user: - Asks "how to configure ruff" - Gets a ruff error and wants to understand how to fix it - Wants to migrate from black/isort/flake8/pylint - Asks about a specific rule (E501, F401, etc.) ## Main Principle: NEVER USE NOQA **Every ruff error has a proper solution.** Using `# noqa` is: - Hiding the problem, not so