FastAPI 测试
FastAPI 基于 Starlette 的 TestClient 提供了便捷的测试支持,可以在不启动服务器的情况下测试 API 的请求和响应。
安装依赖
TestClient 基于 httpx,需要安装:
pip install httpx
基本测试
使用 TestClient 创建测试客户端,然后像使用 httpx 一样发送请求:
实例
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
# 创建测试客户端
client = TestClient(app)
# 测试函数
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
# 创建测试客户端
client = TestClient(app)
# 测试函数
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
TestClient直接调用 FastAPI 应用,不经过网络层,因此测试速度非常快。你不需要启动 Uvicorn 服务器。
测试各种请求类型
实例
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return {"name": item.name, "price": item.price}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, "name": item.name}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
return {"deleted": item_id}
client = TestClient(app)
# 测试 POST 请求
def test_create_item():
response = client.post(
"/items/",
json={"name": "Foo", "price": 42.0}, # 发送 JSON 请求体
)
assert response.status_code == 200
assert response.json() == {"name": "Foo", "price": 42.0}
# 测试 GET 请求
def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1}
# 测试 PUT 请求
def test_update_item():
response = client.put(
"/items/1",
json={"name": "Bar", "price": 50.0},
)
assert response.status_code == 200
# 测试 DELETE 请求
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
assert response.json() == {"deleted": 1}
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return {"name": item.name, "price": item.price}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, "name": item.name}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
return {"deleted": item_id}
client = TestClient(app)
# 测试 POST 请求
def test_create_item():
response = client.post(
"/items/",
json={"name": "Foo", "price": 42.0}, # 发送 JSON 请求体
)
assert response.status_code == 200
assert response.json() == {"name": "Foo", "price": 42.0}
# 测试 GET 请求
def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1}
# 测试 PUT 请求
def test_update_item():
response = client.put(
"/items/1",
json={"name": "Bar", "price": 50.0},
)
assert response.status_code == 200
# 测试 DELETE 请求
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
assert response.json() == {"deleted": 1}
测试查询参数和请求头
实例
from typing import Annotated
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/")
async def read_items(
skip: int = 0,
limit: int = 10,
x_token: Annotated[str | None, Header()] = None,
):
return {"skip": skip, "limit": limit, "x_token": x_token}
client = TestClient(app)
# 测试查询参数
def test_read_items_with_params():
response = client.get("/items/?skip=5&limit=20")
assert response.status_code == 200
assert response.json() == {"skip": 5, "limit": 20, "x_token": None}
# 测试请求头
def test_read_items_with_header():
response = client.get(
"/items/",
headers={"X-Token": "fake-token"}, # 发送自定义请求头
)
assert response.status_code == 200
assert response.json()["x_token"] == "fake-token"
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/")
async def read_items(
skip: int = 0,
limit: int = 10,
x_token: Annotated[str | None, Header()] = None,
):
return {"skip": skip, "limit": limit, "x_token": x_token}
client = TestClient(app)
# 测试查询参数
def test_read_items_with_params():
response = client.get("/items/?skip=5&limit=20")
assert response.status_code == 200
assert response.json() == {"skip": 5, "limit": 20, "x_token": None}
# 测试请求头
def test_read_items_with_header():
response = client.get(
"/items/",
headers={"X-Token": "fake-token"}, # 发送自定义请求头
)
assert response.status_code == 200
assert response.json()["x_token"] == "fake-token"
测试异常响应
实例
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
client = TestClient(app)
# 测试正常响应
def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
# 测试错误响应
def test_read_item_not_found():
response = client.get("/items/0")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
# 测试校验错误
def test_read_item_invalid_id():
response = client.get("/items/foo")
assert response.status_code == 422 # 校验失败
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id}
client = TestClient(app)
# 测试正常响应
def test_read_item():
response = client.get("/items/1")
assert response.status_code == 200
# 测试错误响应
def test_read_item_not_found():
response = client.get("/items/0")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
# 测试校验错误
def test_read_item_invalid_id():
response = client.get("/items/foo")
assert response.status_code == 422 # 校验失败
使用 pytest 运行测试
将测试代码保存为 test_main.py,然后运行:
$ pytest test_main.py # 显示详细输出 $ pytest test_main.py -v # 显示 print 输出 $ pytest test_main.py -s
pytest 会自动发现以 test_ 开头的文件和函数。
测试中覆盖依赖
在测试中,你可能需要覆盖某些依赖(如数据库连接、认证逻辑):
实例
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
# 实际的依赖函数
def get_query_param():
return "real_value"
@app.get("/items/")
async def read_items(query: str = Depends(get_query_param)):
return {"query": query}
client = TestClient(app)
def test_with_override():
# 覆盖依赖
app.dependency_overrides[get_query_param] = lambda: "test_value"
response = client.get("/items/")
assert response.json() == {"query": "test_value"}
# 清除覆盖
app.dependency_overrides.clear()
from fastapi.testclient import TestClient
app = FastAPI()
# 实际的依赖函数
def get_query_param():
return "real_value"
@app.get("/items/")
async def read_items(query: str = Depends(get_query_param)):
return {"query": query}
client = TestClient(app)
def test_with_override():
# 覆盖依赖
app.dependency_overrides[get_query_param] = lambda: "test_value"
response = client.get("/items/")
assert response.json() == {"query": "test_value"}
# 清除覆盖
app.dependency_overrides.clear()
app.dependency_overrides允许你在测试中替换任何依赖函数,非常适合模拟数据库连接、外部 API 调用等场景。测试完成后记得清除覆盖。
小结
- 使用
TestClient直接测试 FastAPI 应用,无需启动服务器 - 支持 GET、POST、PUT、DELETE 等所有 HTTP 方法的测试
- 可以测试查询参数、请求头、请求体、异常响应等
- 使用
app.dependency_overrides在测试中覆盖依赖 - 配合 pytest 可以方便地组织和运行测试
