找回密码
 立即注册
首页 业界区 业界 拒绝“裸奔”上线:FastAPI + Pytest 自动化测试实战指 ...

拒绝“裸奔”上线:FastAPI + Pytest 自动化测试实战指南

东门芳洲 3 小时前
1. 引言:为什么你需要雇佣一群“机器人”?

你是否经历过这种绝望: 你刚刚修复了一个“用户无法登录”的 Bug,满怀信心地推上线。结果两分钟后,老板打电话吼道:“为什么现在的用户没法注册了?!”
这就是典型的回归缺陷(Regression Bug)——修了旧的,坏了新的。
手动测试(用 Postman 一个个点)就像是让主厨在每道菜端出去前都亲自尝一口。这在小餐馆(小项目)行得通,但如果是流水线工厂(大项目),主厨会被撑死,或者因为味觉疲劳而漏掉变质的菜。
自动化测试(Pytest) 就像是雇佣了一万个不知疲倦的机器人。你每改一行代码,它们就能在几秒钟内把所有菜品(API)全部尝一遍,并告诉你:“老板,酸辣汤(登录接口)咸了!”

2. 概念拆解:TestClient 与“替身演员”

FastAPI 的测试之所以简单,是因为它提供了一个神器:TestClient。
核心机制:TestClient

想象一下,通常你测试 API 需要启动服务器,然后用 HTTP 请求去撞它。 而 TestClient 是一种欺骗手段。它不需要真的启动网络服务,它直接通过 Python 代码调用你的 FastAPI 应用函数。

  • 速度极快:没有网络延迟。
  • 同步写法:即使你的 API 是 async 的,测试代码也可以写成普通的同步代码(它是基于 httpx 的)。
核心策略:依赖覆盖(Dependency Override)

这是 FastAPI 测试的杀手锏。还记得我们在“最佳实践”里提到的依赖注入吗? 在测试时,我们不希望操作真实的生产数据库,也不希望真的发送短信验证码。 我们可以用替身(Mock/Override) 来替换掉真实的组件。

3. 动手实战:由浅入深

3.0 准备工作

首先,我们需要安装测试界的“瑞士军刀”:
Bash 
  1. pip install pytest httpx
复制代码
3.1 Hello World 测试 (MVP)

假设你的 main.py 是这样的:
Python 
  1. # main.py
  2. from fastapi import FastAPI
  3. app = FastAPI()
  4. @app.get("/")
  5. async def read_main():
  6.     return {"msg": "Hello World"}
复制代码
我们在同级目录下创建一个 test_main.py:
Python 
  1. # test_main.py
  2. from fastapi.testclient import TestClient
  3. from main import app # 导入你的 app 对象
  4. # 1. 创建测试客户端
  5. client = TestClient(app)
  6. # 2. 编写测试函数(必须以 test_ 开头)
  7. def test_read_main():
  8.     # Act: 模拟发送请求
  9.     response = client.get("/")
  10.    
  11.     # Assert: 断言(验证结果是否符合预期)
  12.     assert response.status_code == 200
  13.     assert response.json() == {"msg": "Hello World"}
复制代码
运行测试: 在终端输入 pytest。你会看到迷人的绿色字体,显示测试通过。
3.2 进阶深潜:搞定数据库测试 (The Real Challenge)

这才是大多数人卡住的地方。如果 API 连了数据库,怎么测? 绝对不要连接生产库测试! 也不要连接开发库,因为测试产生的数据(比如 test_user_1)会污染环境。
解决方案:使用 dependency_overrides 将数据库替换为 SQLite 内存数据库(跑完即焚)。
假设你的 main.py 依赖 get_db:
Python 
  1. # app/dependencies.py
  2. def get_db():
  3.     db = SessionLocal()
  4.     try:
  5.         yield db
  6.     finally:
  7.         db.close()
复制代码
我们来编写一个高级的 test_db.py:
Python 
  1. # test_db.py
  2. from fastapi.testclient import TestClient
  3. from sqlalchemy import create_engine
  4. from sqlalchemy.orm import sessionmaker
  5. from sqlalchemy.pool import StaticPool
  6. from main import app
  7. from app.dependencies import get_db
  8. from app.database import Base
  9. # 1. 创建一个临时的内存数据库(SQLite)
  10. # connect_args={"check_same_thread": False} 是 SQLite 的特殊配置
  11. SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
  12. engine = create_engine(
  13.     SQLALCHEMY_DATABASE_URL,
  14.     connect_args={"check_same_thread": False},
  15.     poolclass=StaticPool # 确保测试过程中连接不中断
  16. )
  17. TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
  18. # 2. 在测试开始前,把表结构建好
  19. Base.metadata.create_all(bind=engine)
  20. # 3. 定义新的依赖函数(替身)
  21. def override_get_db():
  22.     try:
  23.         db = TestingSessionLocal()
  24.         yield db
  25.     finally:
  26.         db.close()
  27. # 4. 【关键步骤】告诉 FastAPI:当有人要 get_db 时,用 override_get_db 顶上去!
  28. app.dependency_overrides[get_db] = override_get_db
  29. client = TestClient(app)
  30. def test_create_user():
  31.     # 这一步请求,实际上是写到了内存数据库里,不会污染真实环境
  32.     response = client.post(
  33.         "/users/",
  34.         json={"email": "test@example.com", "password": "securepassword"},
  35.     )
  36.     assert response.status_code == 200
  37.     data = response.json()
  38.     assert data["email"] == "test@example.com"
  39.     assert "id" in data
复制代码
代码解析:

  • sqlite:///:memory::这是一个存在于 RAM 中的数据库。测试结束,程序退出,它就彻底消失了,干干净净。
  • app.dependency_overrides:这是 FastAPI 专门为测试留的后门。它拦截了所有对 get_db 的调用,并将其重定向到我们的测试数据库。

4. 最佳实践与常见陷阱

陷阱一:测试顺序依赖

错误:测试 A 创建了一个用户,测试 B 尝试登录这个用户。 问题:Pytest 默认是多线程或乱序执行的。如果 B 在 A 之前跑,就挂了。 解决:每个测试函数都应该是独立的。在每个测试开始前初始化数据,或者使用 Pytest 的 fixture 在测试结束后回滚事务。
技巧:使用 conftest.py 共享配置

不要在每个测试文件里都写一遍数据库配置。把通用的配置(比如 client 的初始化、数据库的 override)放到 conftest.py 文件中,Pytest 会自动识别并应用到所有测试。
Python 
  1. # conftest.py (位于测试根目录)
  2. import pytest
  3. from fastapi.testclient import TestClient
  4. from main import app
  5. @pytest.fixture(scope="module")
  6. def client():
  7.     # 这里可以做一些 setup 工作
  8.     with TestClient(app) as c:
  9.         yield c
  10.     # 这里可以做一些 teardown 工作
复制代码

5. 总结与延伸

总结


  • TestClient 是 FastAPI 测试的核心,它快且方便。
  • Dependency Overrides 是解耦的关键,让你能用“假数据库”或“假服务”来测试逻辑。
  • 自动化测试 是你重构代码时的安全网,让你敢于大刀阔斧地修改代码。
课后小作业

回到你的项目中,创建一个 tests 文件夹。

  • 写一个 test_health_check,测试你的 /ping 或 / 接口。
  • 挑战题:尝试测试一个需要 Authentication(登录)的接口。你需要先在测试代码中调用登录接口获取 Token,然后把 Token 放入后续请求的 Header 中:
    Python 
    1. client.get("/users/me", headers={"Authorization": f"Bearer {token}"})
    复制代码
下一步:当你写好了测试,你肯定不想每次都要手动跑 pytest。你想了解如何利用 GitHub Actions (CI/CD),在你每次 git push 代码时自动运行这些测试吗?这样你就可以在睡觉时也确保代码没崩了!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册