skills/alijilani-dev/claude/FastAPI_Pytest_TDDHelper

FastAPI_Pytest_TDDHelper

SKILL.md

FastAPI Pytest TDD Helper

High-performance TDD blueprint for FastAPI projects.

Core Principles

Principle Implementation
Speed AsyncClient over TestClient (~20% faster)
Isolation Transaction rollback, not schema recreation
TDD Red-Green-Refactor cycle strictly
Validation Pydantic models, not just status codes

Quick Start

1. Install Dependencies

pip install pytest pytest-asyncio httpx aiosqlite pytest-cov

2. Configure pytest (pyproject.toml)

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = ["-v", "--tb=short", "-x"]

3. Create conftest.py

import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.pool import NullPool
from app.main import app
from app.database import Base, get_db

@pytest.fixture(scope="session")
async def async_engine():
    engine = create_async_engine("sqlite+aiosqlite:///./test.db", poolclass=NullPool)
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield engine
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)

@pytest.fixture(scope="function")
async def db_session(async_engine):
    async_session = async_sessionmaker(async_engine, class_=AsyncSession)
    async with async_session() as session:
        async with session.begin():
            yield session
            await session.rollback()  # Fast isolation!

@pytest.fixture
async def client(db_session):
    app.dependency_overrides[get_db] = lambda: db_session
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
        yield ac
    app.dependency_overrides.clear()

TDD Workflow: Red-Green-Refactor

Step 1: RED - Write Failing Test

from pydantic import BaseModel

class ItemResponse(BaseModel):
    id: int
    name: str
    price: float

async def test_create_item(client):
    response = await client.post("/items/", json={"name": "Widget", "price": 10.0})

    assert response.status_code == 201
    item = ItemResponse(**response.json())  # Validate shape!
    assert item.name == "Widget"

Run: pytest -x (fails - endpoint doesn't exist)

Step 2: GREEN - Minimal Implementation

@app.post("/items/", status_code=201, response_model=ItemResponse)
async def create_item(item: ItemCreate, db: Session = Depends(get_db)):
    db_item = Item(**item.model_dump())
    db.add(db_item)
    db.commit()
    return db_item

Run: pytest -x (passes)

Step 3: REFACTOR - Optimize

Improve code quality, run tests to verify nothing breaks.

Reference Documentation

Task Reference
conftest.py patterns, fixture scopes references/conftest-patterns.md
Red-Green-Refactor examples references/tdd-workflow.md
pyproject.toml, parallel execution references/pytest-optimization.md
Response validation with Pydantic references/pydantic-validation.md
CRUD tests, mocking, overrides references/testing-patterns.md

Assets

Template Description
assets/conftest_template.py Complete conftest.py ready to customize
assets/pyproject_template.toml Optimized pytest configuration

Performance Decisions

Why AsyncClient Over TestClient

# AVOID: Sync-to-async bridge overhead
from fastapi.testclient import TestClient
client = TestClient(app)

# USE: Native async, ~20% faster
from httpx import AsyncClient, ASGITransport
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
    response = await client.get("/")

Why Transaction Rollback Over Schema Recreation

Approach 100 tests 1000 tests
Schema recreation ~60s ~600s
Transaction rollback ~5s ~50s
# FAST: Rollback at end of each test
async with session.begin():
    yield session
    await session.rollback()

Fixture Scoping Strategy

Scope Use For Example
session Expensive setup DB engine, app instance
function Test isolation DB session with rollback

Common Commands

# Run all tests
pytest

# Run with coverage
pytest --cov=app --cov-report=term-missing

# Run specific test
pytest tests/test_items.py::test_create_item -v

# Run excluding slow tests
pytest -m "not slow"

# Parallel execution
pytest -n auto

# Stop on first failure (TDD mode)
pytest -x

# Run failed tests first
pytest --ff

Parametrize Pattern

@pytest.mark.parametrize("name,price,status", [
    ("Valid", 10.0, 201),
    ("", 10.0, 422),      # Empty name
    ("Item", -5.0, 422),  # Negative price
])
async def test_create_item_validation(client, name, price, status):
    response = await client.post("/items/", json={"name": name, "price": price})
    assert response.status_code == status

Factory Fixture Pattern

@pytest.fixture
def item_factory(db_session):
    async def _create(name="Item", price=10.0, **kwargs):
        item = Item(name=name, price=price, **kwargs)
        db_session.add(item)
        await db_session.flush()
        return item
    return _create

async def test_get_item(client, item_factory):
    item = await item_factory(name="Widget")
    response = await client.get(f"/items/{item.id}")
    assert response.json()["name"] == "Widget"
Weekly Installs
0
First Seen
Jan 1, 1970