依赖覆盖与测试数据库
本教程共 50 篇 · 第 47 篇 · 更新于 2026-08-12 · 约 7 分钟阅读
本节目标:学会用 app.dependency_overrides 在测试里替换依赖(mock 外部服务),并接入一个独立的测试数据库,保证测试不碰生产数据、跑完能清理。
上一章我们能用 TestClient 发请求、做断言。但真实接口常依赖外部东西:一个要收费的身份认证服务、一个真实的数据库。测试时我们不想真的去调那个收费服务,也不想往生产库里塞脏数据。这一章解决这两个问题。
47-1 为什么要覆盖依赖
设想你的接口依赖一个函数 get_reddit_client(),它会去调用第三方 API 拿到数据。每次测试都真调一次,又慢又烧钱。而且第三方服务一旦抽风,你的测试也会跟着挂。
更好的办法是:测试时用一个「假实现」替换掉原依赖,返回固定数据。FastAPI 提供 app.dependency_overrides 这个字典来干这件事——键是原依赖函数,值是你要用的替换函数。
47-2 一个需要外部依赖的应用
# main.py
from fastapi import FastAPI, Depends
app = FastAPI()
def get_reddit_client():
# 真实情况:这里连接第三方、发起网络请求
raise NotImplementedError("连接真实 Reddit 客户端")
@app.get("/ideas/")
async def read_ideas(client=Depends(get_reddit_client)):
top = client.get_reddit_top("recipes")
return top
read_ideas 依赖 get_reddit_client,后者在生产环境会联网。测试里我们不想联网。
47-3 写替换依赖并注册
测试文件里先写一个假的客户端类,再写替换函数返回它。然后把「原依赖 → 替换函数」写进 app.dependency_overrides:
# test_main.py
import pytest
from fastapi.testclient import TestClient
from main import app, get_reddit_client
class FakeRedditClient:
def get_reddit_top(self, subreddit: str) -> dict:
return {
"recipes": ["1: 红烧肉做法"],
"easyrecipes": ["2: 三分钟早餐"],
}
def override_reddit_client():
return FakeRedditClient()
app.dependency_overrides[get_reddit_client] = override_reddit_client
@pytest.fixture
def client():
return TestClient(app)
def test_read_ideas(client):
response = client.get("/ideas/")
assert response.status_code == 200
data = response.json()
assert "recipes" in data
这样 FastAPI 在解析依赖时,会用 override_reddit_client 代替真正的 get_reddit_client,返回我们的假数据,不再联网。
Tip也可以用
from unittest.mock import MagicMock:给mock.get_reddit_top.return_value设好返回值,再让替换函数返回这个 mock。上面用普通假类更直观,二者效果相同。
47-4 只在部分测试里覆盖
如果只想让某个测试用假依赖,其它测试仍用真的,就在那个测试内部设置和清除:
def test_with_override():
app.dependency_overrides[get_reddit_client] = override_reddit_client
client = TestClient(app)
response = client.get("/ideas/")
assert response.status_code == 200
# 测完清掉,恢复原来的依赖
app.dependency_overrides = {}
直接把 app.dependency_overrides 设成空字典 {},就清掉了所有覆盖,原依赖恢复生效。
这个写法简单,但要意识到它是「一刀切」的:如果在 conftest.py 里还统一覆盖了数据库依赖,这一句会把那份覆盖也一并清掉,导致后面的用例莫名连到真实库。想只撤销自己这一个覆盖,用 app.dependency_overrides.pop(get_reddit_client, None) 更稳妥。项目里覆盖项一多,养成用 pop 精确清理的习惯能省下不少排查时间。
Note覆盖可以作用于任何地方用到的依赖:路径操作函数里的、装饰器上的、include_router 里的,FastAPI 都能正确替换。原依赖写在哪不重要。
47-5 测试数据库如何隔离
另一个常见场景是数据库。生产接口通过依赖 get_db() 拿到数据库会话。测试时我们不想写进生产库。做法是:在测试里覆盖 get_db,让它连到一个专门的「测试库」。
下面用 SQLAlchemy 举例(仅演示思路,不需要你此刻掌握数据库)。生产依赖:
# deps.py
from sqlalchemy.orm import Session
from .db.session import SessionLocal
def get_db():
db: Session = SessionLocal()
try:
yield db
finally:
db.close()
测试库用一个单独的 SQLite 文件 test.db,避免动生产数据:
# conftest.py(与 main.py 同目录)
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app
from deps import get_db
TEST_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False},
)
TestingSessionLocal = sessionmaker(bind=engine)
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
@pytest.fixture
def client():
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
app.dependency_overrides = {}
这里 conftest.py 和 main.py 放在同一个目录,用绝对导入 from main import app、from deps import get_db 即可(deps.py 里就是上面的生产 get_db)。如果你的应用像第 45 章那样放在 app/ 包里,则相应改成 from app.main import app、from app.dependencies import get_db,保持导入路径与项目结构一致。
覆盖后,所有用到 Depends(get_db) 的接口,在测试中都连到了 test.db。生产库完全不受影响。
47-6 为什么必须隔离
测试会反复增删数据。如果直接写进生产库:
- 测试产生的垃圾数据会污染真实库
- 多个测试并发跑可能互相干扰,导致断言时读到的数据不是自己写的
- 测试失败留下的脏数据会影响下次运行,乃至影响线上用户
用独立测试库,等于给测试划了一块「沙盒」。怎么折腾都不波及生产。这也是测试数据库存在的核心意义。
47-7 测试后清理
测试库每次跑完应当回到干净状态,否则上次残留的数据可能让下次断言出错。常见两种清理方式。
第一,每次测试前后重建表。在 fixture 里用 with TestClient(app) 的上下文执行建表/删表:
@pytest.fixture
def client():
# 测试前:建表
Base.metadata.create_all(bind=engine)
app.dependency_overrides[deps.get_db] = override_get_db
with TestClient(app) as c:
yield c
# 测试后:清掉覆盖并删表
app.dependency_overrides = {}
Base.metadata.drop_all(bind=engine)
第二,用事务回滚。测试开始时开一个事务,结束时回滚,数据自动消失,比删除重建更快。SQLAlchemy 配合 session.rollback() 就能做到,适合用例多的项目。
还有一种更彻底的思路是把测试库放进内存,连接字符串写成 sqlite:///:memory:。这样连磁盘文件都不产生,进程结束数据自动蒸发,速度也最快。代价是每次新建连接都会得到一个全新的空库,所以必须让整轮测试共用同一个连接,通常靠给引擎配置 StaticPool 来实现。用例不多时,前面那种「建表、删表」的文件库方案更好理解,也更容易在出问题时打开文件看一眼数据到底写成了什么样。
Tip把「设置覆盖」放在 fixture 的
yield之前,「清理覆盖和删表」放在yield之后。这样每个测试开始前都拿到干净环境,结束自动复位,互不干扰。
47-8 结合请求断言的完整例子
def test_create_and_read(client):
# 往测试库写一条
r1 = client.post("/recipes/", json={"title": "番茄炒蛋"})
assert r1.status_code == 200
# 再读出来
r2 = client.get("/recipes/")
assert r2.status_code == 200
assert any(item["title"] == "番茄炒蛋" for item in r2.json())
这个用例全程作用于 test.db,跑完被 fixture 清理掉,生产库始终干净。
47-9 小结
测试中想换掉外部依赖,就用 app.dependency_overrides[原依赖] = 替换函数,它支持任何位置的依赖;测完设成 {} 即可清除。涉及数据库时,覆盖 get_db 指向独立的测试库,既快又能隔离生产数据,并用 fixture 在测试前后做建表/删表或回滚来清理。