FastAPI¶
Status: Fully supported (via Starlette extractor)
FastAPI is built on Starlette, so pytest-routes uses the Starlette extractor with FastAPI-specific enhancements.
Installation¶
pip install "pytest-routes[fastapi]"
uv add "pytest-routes[fastapi]"
Features¶
Path parameter extraction from route patterns
Query parameter extraction from endpoint signatures
Pydantic model body extraction with full validation
Automatic type inference from annotations
Response model extraction for validation
Complete Example¶
# myapp/main.py
from uuid import UUID
from typing import Annotated
from fastapi import FastAPI, Query, Path, Body
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=100)
email: EmailStr | None = None
is_active: bool | None = None
class User(UserBase):
id: UUID
is_active: bool = True
class Config:
from_attributes = True
app = FastAPI(title="User API", version="1.0.0")
@app.get("/users/", response_model=list[User], tags=["users"])
async def list_users(
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
active_only: Annotated[bool, Query()] = False,
) -> list[User]:
"""List all users with pagination."""
return []
@app.get("/users/{user_id}", response_model=User, tags=["users"])
async def get_user(user_id: Annotated[UUID, Path()]) -> User:
"""Get a specific user by ID."""
return User(id=user_id, name="Test", email="test@example.com")
@app.post("/users/", response_model=User, status_code=201, tags=["users"])
async def create_user(user: Annotated[UserCreate, Body()]) -> User:
"""Create a new user."""
return User(id=UUID("12345678-1234-1234-1234-123456789012"),
name=user.name, email=user.email)
@app.put("/users/{user_id}", response_model=User, tags=["users"])
async def update_user(
user_id: Annotated[UUID, Path()],
user: Annotated[UserUpdate, Body()],
) -> User:
"""Update an existing user."""
return User(id=user_id, name=user.name or "Updated",
email=user.email or "updated@example.com")
@app.delete("/users/{user_id}", status_code=204, tags=["users"])
async def delete_user(user_id: Annotated[UUID, Path()]) -> None:
"""Delete a user."""
pass
Running Tests¶
# Basic smoke test
pytest --routes --routes-app myapp.main:app
# Test with more examples
pytest --routes --routes-app myapp.main:app --routes-max-examples 200
# Test only GET endpoints (safe for real data)
pytest --routes --routes-app myapp.main:app --routes-methods GET
Tips¶
Tip
Use Pydantic models with constraints. FastAPI’s Field() constraints
(min_length, ge, le, etc.) help pytest-routes generate valid data.
Tip
Annotated types are fully supported. Use Annotated[int, Query(ge=0)]
for better documentation and constraint extraction.
Warning
Dependency injection is not automatically invoked. For routes with complex dependencies (database sessions, auth), configure overrides in your test fixture.
Handling Dependencies¶
# conftest.py
import pytest
@pytest.fixture
def app():
"""Create app with dependency overrides."""
from myapp.main import app
from myapp.deps import get_db, get_current_user
from myapp.testing import TestDatabase, TestUser
app.dependency_overrides[get_db] = lambda: TestDatabase()
app.dependency_overrides[get_current_user] = lambda: TestUser()
yield app
app.dependency_overrides.clear()