AgentSkillsCN

FastAPI_Pytest_TDDHelper

资深后端架构师兼首席质量保证工程师,精通基于FastAPI与Pytest的测试驱动开发(TDD)。提供高性能测试方案,优先考虑执行速度与内存效率。适用场景如下:(1) 为FastAPI项目配置Pytest;(2) 按照红-绿-重构的TDD循环编写测试;(3) 使用异步Fixture构建高性能conftest.py;(4) 实施事务回滚模式,确保测试隔离性高效快速;(5) 采用httpx.AsyncClient进行异步端点测试;(6) 利用Pydantic模型对响应进行校验;(7) 创建工厂Fixture与依赖项覆盖;(8) 优化测试执行速度与并行化能力。适用于FastAPI v0.100+与Pytest v8.0+。

SKILL.md
--- frontmatter
name: FastAPI_Pytest_TDDHelper
description: |
  Senior Backend Architect and Principal QA Engineer skill for Test-Driven Development (TDD) with FastAPI and Pytest. Provides high-performance testing blueprints prioritizing execution speed and memory efficiency. Use when: (1) Setting up pytest for FastAPI projects, (2) Writing tests following Red-Green-Refactor TDD cycle, (3) Creating high-performance conftest.py with async fixtures, (4) Implementing transaction rollback patterns for fast test isolation, (5) Using httpx.AsyncClient for async endpoint testing, (6) Validating responses with Pydantic models, (7) Creating factory fixtures and dependency overrides, (8) Optimizing test execution speed and parallelization. Applies to FastAPI v0.100+ and Pytest v8.0+.

FastAPI Pytest TDD Helper

High-performance TDD blueprint for FastAPI projects.

Core Principles

PrincipleImplementation
SpeedAsyncClient over TestClient (~20% faster)
IsolationTransaction rollback, not schema recreation
TDDRed-Green-Refactor cycle strictly
ValidationPydantic models, not just status codes

Quick Start

1. Install Dependencies

bash
pip install pytest pytest-asyncio httpx aiosqlite pytest-cov

2. Configure pytest (pyproject.toml)

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

3. Create conftest.py

python
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

python
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

python
@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

TaskReference
conftest.py patterns, fixture scopesreferences/conftest-patterns.md
Red-Green-Refactor examplesreferences/tdd-workflow.md
pyproject.toml, parallel executionreferences/pytest-optimization.md
Response validation with Pydanticreferences/pydantic-validation.md
CRUD tests, mocking, overridesreferences/testing-patterns.md

Assets

TemplateDescription
assets/conftest_template.pyComplete conftest.py ready to customize
assets/pyproject_template.tomlOptimized pytest configuration

Performance Decisions

Why AsyncClient Over TestClient

python
# 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

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

Fixture Scoping Strategy

ScopeUse ForExample
sessionExpensive setupDB engine, app instance
functionTest isolationDB session with rollback

Common Commands

bash
# 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

python
@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

python
@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"