1. 多模块集成测试实践
FastAPI 的集成测试核心在于模拟真实环境中的多个服务模块交互,尤其针对认证、数据库、外部服务等场景。
1.1 测试框架与工具链
- 工具选择 :使用
pytest
+httpx
+asyncio
组合 - Mock 策略 :通过
unittest.mock
替换外部依赖 - 数据库隔离 :使用
pytest-asyncio
管理异步事务回滚
python
# 安装依赖
# pip install pytest==7.4.0 httpx==0.25.0 pytest-asyncio==0.21.1
1.2 多模块协同测试模型
flowchart TB
A[测试入口] --> B[模拟认证模块]
B --> C[调用用户服务模块]
C --> D[触发支付服务模块]
D --> E[验证结果]
1.3 实战案例:订单支付链路测试
python
from fastapi.testclient import TestClient
from unittest.mock import patch
import pytest
# 依赖模拟
@pytest.fixture
def mock_payment_gateway():
with patch("app.services.payment.PaymentGateway.charge") as mock:
mock.return_value = {"status": "success"}
yield
# 测试逻辑
def test_payment_flow(client: TestClient, mock_payment_gateway):
# 1. 获取认证Token
auth = client.post("/auth/login", json={"username": "test", "password": "pass"})
token = auth.json()["access_token"]
# 2. 创建订单
order = client.post(
"/orders",
json={"product_id": "p123", "qty": 2},
headers={"Authorization": f"Bearer {token}"}
)
order_id = order.json()["id"]
# 3. 执行支付
payment = client.post(
f"/orders/{order_id}/pay",
json={"card": "4111111111111111"},
headers={"Authorization": f"Bearer {token}"}
)
# 4. 验证结果
assert payment.status_code == 200
assert payment.json()["status"] == "completed"
关键点说明:
- 通过
mock_payment_gateway
隔离第三方支付服务 - 复用同一个
TestClient
维护请求上下文 - 使用
fixture
管理测试生命周期
2. 带认证上下文的端到端测试
2.1 认证机制实现原理
FastAPI 通过依赖注入系统管理认证流程:
flowchart LR
R[请求] --> D[认证依赖]
D -->|成功| U[用户对象]
D -->|失败| E[401错误]
2.2 OAuth2 测试策略
python
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
# 验证逻辑(实际项目需连接数据库)
return {"username": "test"} if token == "valid_token" else None
# 测试中伪造Token
def test_protected_route():
client = TestClient(app)
response = client.get("/protected", headers={"Authorization": "Bearer valid_token"})
assert response.status_code == 200
2.3 自动化认证流水线
python
class AuthContext:
def __init__(self, client: TestClient):
self.client = client
self.token = None
def login(self, username, password):
res = self.client.post("/auth/login", json={"username": username, "password": password})
self.token = res.json().get("access_token")
return self
def get_headers(self):
return {"Authorization": f"Bearer {self.token}"}
# 测试用例
def test_user_profile():
auth = AuthContext(client).login("test", "pass")
res = client.get("/profile", headers=auth.get_headers())
assert res.json()["username"] == "test"
课后 Quiz
问题:如何避免认证测试中的Token硬编码风险?
解析:采用动态Token生成策略:
- 在测试配置中引入真实认证服务
- 使用环境变量管理测试账户凭证
- 通过
@pytest.fixture
动态获取Token
正确实践:
python
@pytest.fixture
def valid_token(client: TestClient):
res = client.post("/auth/login", json={"username": "test", "password": "pass"})
return res.json()["access_token"]
def test_with_dynamic_token(client: TestClient, valid_token):
res = client.get("/protected", headers={"Authorization": f"Bearer {valid_token}"})
assert res.status_code == 200
常见报错解决方案
报错:422 Validation Error
原因分析:
- 请求体数据结构不符合Pydantic模型
- 路径参数类型错误
解决方案:
python
# 错误示例
client.post("/orders", json={"product": "p123"}) # 缺少必要字段qty
# 正确方式
client.post("/orders", json={"product_id": "p123", "qty": 1})
预防建议:
- 在路由中使用
response_model
自动校验
python
@app.post("/orders", response_model=OrderResponse)
- 为模型字段设置默认值或可选标记
python
class Order(BaseModel):
product_id: str
qty: int = 1 # 默认值
notes: Optional[str] = None # 可选字段
报错:401 Unauthorized
解决方案:
- 检查依赖的
get_current_user
逻辑 - 验证测试中Token的生成算法与认证逻辑是否一致
- 使用中间件打印请求头信息:
python
@app.middleware("http")
async def debug_middleware(request: Request, call_next):
print("Headers:", request.headers)
return await call_next(request)