---
description: "从项目一开始就用异步测试客户端，不要等到后期再改——会遇到事件循环错误"
---

# FastAPI 测试

## 核心原则

从项目一开始就用异步测试客户端，不要等到后期再改——会遇到事件循环错误。

## 异步测试客户端

用 HTTPX 的 `AsyncClient` 替代 Starlette 的 `TestClient`：

```python
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` 包：

```python
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 的依赖自动安装）：

```python
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）：

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

## 跨模块测试

用 fixture 封装前置条件（如认证），测试本身只关注「发请求 → 验响应」：

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