FastAPI-07.测试与项目骨架

本系列:00 导读 · 01 心智模型 · 02 路由与数据模型 · 03 依赖注入与分层 · 04 中间件异常日志 · 05 异步后台与流式 · 06 鉴权与安全 · 07 测试与项目骨架(本文) · 08 实战 HTTP↔MCP

行文:T4 项目篇 | 本篇方法:极度学习 + 科尔布循环 | 辅助:刻意练习


1. 成功标准(验收)

从空目录出发,最终应满足:

  • uvicorn app.main:app 可启动,/docs 可开
  • 至少 1 个 router + service + schema
  • pytest 绿,覆盖健康检查 + 1 条业务 API
  • 测试里用 dependency_overrides 假 DB,不连真库
  • 能用一段话讲清「请求进哪一层」

2. 推荐骨架(直接任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fastapi-demo/
├── pyproject.toml
├── .env.example
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/
│ │ ├── config.py
│ │ └── deps.py
│ ├── routers/
│ │ ├── health.py
│ │ └── items.py
│ ├── services/
│ │ └── items.py
│ ├── schemas/
│ │ └── items.py
│ └── repositories/
│ └── items.py # 可先内存 dict
└── tests/
├── conftest.py
└── test_items.py

pyproject.toml(最小依赖)

1
2
3
4
5
6
7
8
9
10
11
12
[project]
name = "fastapi-demo"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"pydantic-settings>=2.0.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0.0", "httpx>=0.27.0"]

安装:uv sync --extra dev(或 pip install -e ".[dev]")。

app/core/config.py

1
2
3
4
5
6
7
8
9
10
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
app_name: str = "fastapi-demo"
debug: bool = False


settings = Settings()

app/main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.core.config import settings
from app.routers import health, items


@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.ready = True
yield
app.state.ready = False


app = FastAPI(title=settings.app_name, lifespan=lifespan)
app.include_router(health.router)
app.include_router(items.router)

app/routers/health.py

1
2
3
4
5
6
7
8
from fastapi import APIRouter, Request

router = APIRouter(tags=["health"])


@router.get("/health")
async def health(request: Request):
return {"status": "ok", "ready": request.app.state.ready}

3. 测试:TestClient + overrides

tests/conftest.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pytest
from fastapi.testclient import TestClient

from app.core.deps import get_db
from app.main import app


class FakeDB:
def __init__(self):
self.items = {1: {"id": 1, "name": "tea", "price": 9.9}}


@pytest.fixture
def fake_db():
return FakeDB()


@pytest.fixture
def client(fake_db):
def override_get_db():
yield fake_db

app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()

tests/test_items.py

1
2
3
4
5
6
7
8
9
def test_get_item(client):
r = client.get("/items/1")
assert r.status_code == 200
assert r.json()["name"] == "tea"


def test_get_item_not_found(client):
r = client.get("/items/999")
assert r.status_code == 404

TestClient 同步调用 ASGI 应用,不需要起真实 uvicorn 端口。


4. 科尔布四拍(建议写进学习笔记)

阶段 你做什么 本篇对应
具体经验 按骨架敲一遍,跑通 /health §2
反思 卡在哪?import 循环?override 忘 clear? 记录 3 条
抽象 提炼规则:如「测试只 override 边界依赖」 见下
再实验 加一条 POST + 422 测试 drills

可复用三条规则

  1. 应用工厂appmain.py 单例,测试 import 同一实例才能 override。
  2. 假实现挂在 deps:repo 可内存实现,router 不改。
  3. 每个测试清理 overrides:放 fixture yieldclear()

5. 反馈回路

1
2
3
4
5
6
7
8
# 运行
uvicorn app.main:app --reload

# 测试
pytest -q

# 类型(可选)
# pyright app 或 mypy app

6. 费曼收束(一页纸提纲)

向同事讲解时覆盖:

  1. 请求进 router 之前经过了谁(中间件、Depends)?
  2. servicerepository 各一句话职责?
  3. 测试为什么不用真数据库也能测 404?

7. 闪卡候选

正面 背面
TestClient 测的是什么? ASGI 应用,非 TCP 端口
override 用完要? dependency_overrides.clear()
lifespan 在测试里跑吗? TestClient 上下文会触发
配置推荐? pydantic-settings + .env

小结

-------------本文结束感谢您的阅读-------------