FastAPI 测试
核心原则
从项目一开始就用异步测试客户端,不要等到后期再改——会遇到事件循环错误。
异步测试客户端
用 HTTPX 的 AsyncClient 替代 Starlette 的 TestClient:
import pytest
from httpx import AsyncClient, ASGITransport
from src.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
如果用了 lifespan 事件(startup/shutdown),需要 asgi-lifespan 包:
from asgi_lifespan import LifespanManager
from httpx import AsyncClient, ASGITransport
@pytest.fixture
async def client():
async with LifespanManager(app) as manager:
transport = ASGITransport(app=manager.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
测试标记
用 pytest.mark.anyio 替代 pytest.mark.asyncio(anyio 已作为 Starlette 的依赖自动安装):
import pytest
@pytest.mark.anyio
async def test_create_post(client: AsyncClient):
resp = await client.post("/posts", json={"title": "Test", "content": "Hello"})
assert resp.status_code == 201
限定只跑 asyncio 后端(否则 anyio 默认还会跑一遍 trio):
跨模块测试
用 fixture 封装前置条件(如认证),测试本身只关注「发请求 → 验响应」:
@pytest.fixture
async def auth_headers(client: AsyncClient):
await client.post("/auth/register", json={...})
resp = await client.post("/auth/login", json={...})
token = resp.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.mark.anyio
async def test_create_post_requires_auth(client: AsyncClient):
resp = await client.post("/posts", json={"title": "No Auth"})
assert resp.status_code == 401
@pytest.mark.anyio
async def test_create_post_success(client: AsyncClient, auth_headers: dict):
resp = await client.post("/posts", json={"title": "OK"}, headers=auth_headers)
assert resp.status_code == 201
测试目录结构
与 src 目录一一镜像:
tests/
├── conftest.py # 共享 fixture(client, auth_headers)
├── auth/
│ └── test_login.py
├── posts/
│ └── test_create.py
└── users/
└── test_profile.py
关键原则
- 测接口行为,不测内部实现。跨模块调用对测试来说是透明的
- 尽量少 mock,走真实链路(集成测试)。只有调外部第三方 API 时才 mock
- 用 fixture 封装前置条件,测试用例本身只关注「发请求 → 验响应」