首页 / FastAPI 入门教程 / 依赖覆盖与测试数据库

FastAPI 入门教程

依赖覆盖与测试数据库

本教程共 50 篇 · 第 47 篇 · 更新于 2026-08-12 · 约 7 分钟阅读

FastAPIFastAPI 入门教程依赖覆盖测试数据库dependency_overridesmock

本节目标:学会用 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.pymain.py 放在同一个目录,用绝对导入 from main import appfrom deps import get_db 即可(deps.py 里就是上面的生产 get_db)。如果你的应用像第 45 章那样放在 app/ 包里,则相应改成 from app.main import appfrom 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 在测试前后做建表/删表或回滚来清理。