返回 Skill 列表
extension
分类: 开发与工程无需 API Key

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+。

person作者: jakexiaohubgithub

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"