了解如何使用 TestClient 有效地测试 FastAPI 应用程序。涵盖最佳实践、高级技术和真实示例,以构建强大而可靠的 API。
精通 FastAPI 测试:TestClient 综合指南
FastAPI 已经成为使用 Python 构建高性能 API 的领先框架。它的速度、易用性和自动数据验证使其成为全球开发人员的最爱。然而,一个构建良好的 API 只有在经过充分测试后才能发挥其作用。彻底的测试确保您的 API 按预期运行,在压力下保持稳定,并且可以自信地部署到生产环境。本综合指南重点介绍如何使用 FastAPI 的 TestClient 来有效地测试您的 API 端点。
为什么测试对 FastAPI 应用程序很重要?
测试是软件开发生命周期中的关键步骤。它可以帮助您:
- 尽早发现错误:在错误到达生产环境之前捕获它们,节省时间和资源。
- 确保代码质量:促进结构良好且可维护的代码。
- 防止回归:保证新更改不会破坏现有功能。
- 提高 API 可靠性:建立对 API 的稳定性和性能的信心。
- 促进协作:为其他开发人员提供预期行为的清晰文档。
介绍 FastAPI 的 TestClient
FastAPI 提供了一个内置的 TestClient,可以简化测试 API 端点的过程。TestClient 充当一个轻量级客户端,可以向您的 API 发送请求,而无需启动一个完整的服务器。这使得测试速度更快、更方便。
TestClient 的主要功能:
- 模拟 HTTP 请求:允许您向您的 API 发送 GET、POST、PUT、DELETE 和其他 HTTP 请求。
- 处理数据序列化:自动序列化请求数据(例如,JSON 有效负载)和反序列化响应数据。
- 提供断言方法:提供方便的方法来验证响应的状态代码、标头和内容。
- 支持异步测试:与 FastAPI 的异步特性无缝协作。
- 与测试框架集成:轻松地与流行的 Python 测试框架(如 pytest 和 unittest)集成。
设置您的测试环境
在开始测试之前,您需要设置您的测试环境。这通常涉及安装必要的依赖项和配置您的测试框架。
安装
首先,确保您已经安装了 FastAPI 和 pytest。您可以使用 pip 安装它们:
pip install fastapi pytest httpx
httpx 是 FastAPI 在底层使用的 HTTP 客户端。虽然 TestClient 是 FastAPI 的一部分,但同时安装 httpx 可以确保测试顺利进行。一些教程还提到了 requests,但是,httpx 更符合 FastAPI 的异步特性。
示例 FastAPI 应用程序
让我们创建一个简单的 FastAPI 应用程序,我们可以用它来进行测试:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
将此代码保存为 main.py。此应用程序定义了三个端点:
/: 一个简单的 GET 端点,返回 "Hello World" 消息。/items/{item_id}: 一个 GET 端点,根据其 ID 返回一个项目。/items/: 一个 POST 端点,创建一个新项目。
编写您的第一个测试
现在您有了一个 FastAPI 应用程序,您可以使用 TestClient 开始编写测试。在与 main.py 相同的目录中创建一个名为 test_main.py 的新文件。
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
在这个测试中:
- 我们导入
TestClient和 FastAPIapp实例。 - 我们创建一个
TestClient的实例,传入app。 - 我们定义一个测试函数
test_read_root。 - 在测试函数内部,我们使用
client.get("/")向根端点发送一个 GET 请求。 - 我们断言响应状态代码为 200 (OK)。
- 我们断言响应 JSON 等于
{"message": "Hello World"}。
使用 pytest 运行您的测试
要运行您的测试,只需在包含您的 test_main.py 文件的目录中打开一个终端,并运行以下命令:
pytest
pytest 将自动发现并运行您项目中的所有测试。您应该看到类似于以下的输出:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
测试不同的 HTTP 方法
TestClient 支持所有标准的 HTTP 方法,包括 GET、POST、PUT、DELETE 和 PATCH。让我们看看如何测试这些方法中的每一种。
测试 GET 请求
我们已经在前面的章节中看到了一个测试 GET 请求的例子。这里是另一个例子,测试 /items/{item_id} 端点:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
这个测试向 /items/1 发送一个带有查询参数 q=test 的 GET 请求。然后,它断言响应状态代码为 200,并且响应 JSON 包含预期的数据。
测试 POST 请求
要测试 POST 请求,您需要在请求正文中发送数据。TestClient 会自动将数据序列化为 JSON。
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
在这个测试中:
- 我们创建一个字典
item_data,其中包含新项目的数据。 - 我们使用
client.post("/items/", json=item_data)向/items/端点发送一个 POST 请求,并将item_data作为 JSON 有效负载传递。 - 我们断言响应状态代码为 200,并且响应 JSON 与
item_data匹配。
测试 PUT、DELETE 和 PATCH 请求
测试 PUT、DELETE 和 PATCH 请求类似于测试 POST 请求。您只需在 TestClient 上使用相应的方法:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
记住要添加断言来验证响应是否符合预期。
高级测试技术
TestClient 提供了几个高级功能,可以帮助您编写更全面和有效的测试。
测试依赖项
FastAPI 的依赖注入系统允许您轻松地将依赖项注入到您的 API 端点中。在测试时,您可能想要覆盖这些依赖项,以提供模拟的或特定于测试的实现。
例如,假设您的应用程序依赖于数据库连接。您可以在您的测试中覆盖数据库依赖项,以使用内存数据库:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
这个例子覆盖了 get_db 依赖项,使用一个特定于测试的函数,该函数返回一个到内存 SQLite 数据库的会话。重要提示:必须显式调用元数据创建才能使测试数据库正常运行。未能创建表将导致与缺少表相关的错误。
测试异步代码
FastAPI 构建为异步的,因此您通常需要测试异步代码。TestClient 无缝地支持异步测试。
要测试一个异步端点,只需将您的测试函数定义为 async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
注意:您需要安装 pytest-asyncio 才能使用 @pytest.mark.asyncio:pip install pytest-asyncio。如果使用较旧的 pytest 版本,您还需要确保配置了 asyncio.get_event_loop()。如果使用 pytest 8 或更新版本,则可能不需要这样做。
测试文件上传
FastAPI 可以轻松地处理文件上传。要测试文件上传,您可以使用 TestClient 的请求方法的 files 参数。
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
在这个测试中,我们使用 io.BytesIO 创建一个虚拟文件,并将其传递给 files 参数。files 参数接受一个元组列表,其中每个元组包含字段名称、文件名和文件内容。内容类型对于服务器的准确处理非常重要。
测试错误处理
测试您的 API 如何处理错误非常重要。您可以使用 TestClient 发送无效的请求,并验证 API 是否返回正确的错误响应。
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
这个测试向 /items/101 发送一个 GET 请求,该请求会引发一个状态代码为 400 的 HTTPException。测试断言响应状态代码为 400,并且响应 JSON 包含预期的错误消息。
测试安全功能
如果您的 API 使用身份验证或授权,您还需要测试这些安全功能。TestClient 允许您设置标头和 Cookie 来模拟经过身份验证的请求。
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
在这个例子中,我们测试登录端点,然后使用获取的 token 来访问一个受保护的路由。TestClient 的请求方法的 headers 参数允许您设置自定义标头,包括用于 bearer token 的 Authorization 标头。
FastAPI 测试的最佳实践
以下是一些在测试您的 FastAPI 应用程序时应遵循的最佳实践:
- 编写全面的测试:争取高测试覆盖率,以确保您的 API 的所有部分都经过彻底的测试。
- 使用描述性的测试名称:确保您的测试名称清楚地表明测试正在验证什么。
- 遵循 Arrange-Act-Assert 模式:将您的测试组织成三个不同的阶段:Arrange(设置测试数据)、Act(执行正在测试的操作)和 Assert(验证结果)。
- 使用模拟对象:模拟外部依赖项,以隔离您的测试并避免依赖外部系统。
- 测试边缘情况:使用无效的或意外的输入来测试您的 API,以确保它能优雅地处理错误。
- 经常运行测试:将测试集成到您的开发工作流程中,以便尽早和经常地捕获错误。
- 与 CI/CD 集成:在您的 CI/CD 管道中自动化您的测试,以确保所有代码更改在部署到生产环境之前都经过彻底的测试。可以使用 Jenkins、GitLab CI、GitHub Actions 或 CircleCI 等工具来实现这一点。
示例:国际化 (i18n) 测试
在为全球受众开发 API 时,国际化 (i18n) 至关重要。测试 i18n 涉及验证您的 API 是否正确支持多种语言和地区。以下是如何在 FastAPI 应用程序中测试 i18n 的示例:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
这个例子设置 Accept-Language 标头来指定所需的语言。API 返回指定语言的问候语。测试确保 API 正确处理不同的语言首选项。如果缺少 Accept-Language 标头,则使用默认的 "en" 语言。
结论
测试是构建强大且可靠的 FastAPI 应用程序的重要组成部分。TestClient 提供了一种简单方便的方法来测试您的 API 端点。通过遵循本指南中概述的最佳实践,您可以编写全面的测试,以确保您的 API 的质量和稳定性。从基本请求到依赖注入和异步测试等高级技术,TestClient 使您能够创建经过良好测试且可维护的代码。将测试作为您开发工作流程的核心部分来拥抱,您将构建出既强大又可靠的 API,为全球用户提供服务。请记住 CI/CD 集成的重要性,以自动执行测试并确保持续的质量保证。