跳转至

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):

@pytest.fixture
def anyio_backend():
    return "asyncio"

跨模块测试

用 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 封装前置条件,测试用例本身只关注「发请求 → 验响应」