1. 需求驱动测试用例设计
1.1 什么是需求驱动测试
需求驱动测试(Requirement-Driven Testing)是在测试驱动开发(TDD)中先根据需求定义测试用例,再实现功能的开发方法。在FastAPI开发中,这意味着:
- 先分析API接口需求文档(如OpenAPI规范)
- 将需求转化为具体的测试断言
- 编写失败测试(Red阶段)
- 逐步实现功能使测试通过(Green阶段)
这能确保代码精确满足需求且具备可测性。
1.2 测试用例设计流程
graph TD
A[分析API需求] --> B[定义输入/输出]
B --> C[编写Pydantic模型]
C --> D[创建测试断言]
D --> E[实现业务逻辑]
E --> F[重构优化]
1.3 典型测试场景
在FastAPI中需要覆盖:
- ✅ HTTP状态码验证
- ✅ 响应数据结构验证
- ✅ 错误处理逻辑
- ✅ 权限验证
- ✅ 数据验证规则
2. 实战案例:用户注册API
假设需求文档要求:
- 通过POST /register注册新用户
- 必填字段:username(5-20字符), password(8+字符)
- 用户名冲突返回409
- 成功返回201并包含用户ID
2.1 测试用例实现
python
# test_user_api.py
# 所需依赖:pytest==7.1.2, httpx==0.23.0
from fastapi.testclient import TestClient
from pydantic import BaseModel
import pytest
class UserCreate(BaseModel):
username: str
password: str
# 测试类封装
class TestUserRegistration:
@pytest.fixture(autouse=True)
def setup(self, client: TestClient):
self.client = client
self.url = "/register"
def test_successful_registration(self):
"""需求1&4:验证成功注册"""
response = self.client.post(
self.url,
json={"username": "new_user", "password": "StrongPass123"}
)
assert response.status_code == 201
assert "user_id" in response.json()
def test_username_conflict(self):
"""需求3:验证用户名冲突"""
# 先创建测试用户
self.client.post(self.url, json={
"username": "existing",
"password": "Password123"
})
# 再次使用相同用户名
response = self.client.post(
self.url,
json={"username": "existing", "password": "NewPass456"}
)
assert response.status_code == 409
assert response.json()["detail"] == "Username already exists"
def test_invalid_password(self):
"""需求2:验证密码规则"""
response = self.client.post(
self.url,
json={"username": "short_pass", "password": "abc"}
)
assert response.status_code == 422
errors = response.json()["detail"]
assert any("ensure this value has at least 8 characters" in e["msg"] for e in errors)
2.2 业务逻辑实现
python
# main.py
# 所需依赖:fastapi==0.78.0, pydantic==1.10.2
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, constr
app = FastAPI()
mock_db = []
# 使用Pydantic严格数据约束
class UserCreate(BaseModel):
username: constr(min_length=5, max_length=20)
password: constr(min_length=8)
@app.post("/register", status_code=status.HTTP_201_CREATED)
def register_user(user: UserCreate):
"""实现用户注册逻辑"""
# 检查用户名冲突
if any(u["username"] == user.username for u in mock_db):
raise HTTPException(
status_code=409,
detail="Username already exists"
)
# 创建用户记录(模拟DB插入)
new_user = {
"user_id": len(mock_db) + 1,
"username": user.username
}
mock_db.append(new_user)
return new_user
2.3 关键实现说明
- 数据验证 :使用
constr
限制字段长度,自动返回422错误 - 错误处理:针对冲突场景返回409状态码
- 响应结构:严格匹配测试定义的响应字段
- 状态管理 :使用
status
模块保证状态码常量正确性
3. 课后Quiz
问题1
当测试返回422错误时,通常表示什么类型的问题?
A) 服务器内部错误
B) 权限验证失败
C) 请求数据验证失败
D) 路由不存在
查看答案 C) 请求数据验证失败 解析:FastAPI通过Pydantic进行自动数据验证,不符合模型约束的请求会返回422 Unprocessable Entity
问题2
在需求驱动测试中,应该何时编写业务逻辑代码?
A) 在编写测试用例之前
B) 与测试用例同时编写
C) 在测试用例失败之后
D) 在所有测试设计完成后
查看答案 C) 在测试用例失败之后 解析:TDD标准流程是Red-Green-Refactor,先编写失败测试,再实现功能使测试通过
问题3
如何处理API的多版本兼容测试需求?
A) 为每个版本复制测试套件
B) 使用参数化测试覆盖不同版本
C) 忽略老版本测试
D) 在路由中使用版本前缀
python
# 参考答案B示例
@pytest.mark.parametrize("version", ["v1", "v2"])
def test_api_version_compatibility(client, version):
response = client.get(f"/{version}/users")
assert response.status_code == 200
4. 常见报错解决方案
4.1 422 Validation Error
触发场景:
json
{
"detail": [
{
"loc": ["body", "password"],
"msg": "ensure this value has at least 8 characters",
"type": "value_error.any_str.min_length"
}
]
}
解决方案:
- 检查请求数据是否符合Pydantic模型定义
- 使用OpenAPI文档验证数据结构
- 添加默认值或Optional字段处理可选参数
- 自定义错误消息提升可读性:
python
password: constr(
min_length=8,
error_msg="密码长度至少8个字符"
)
4.2 405 Method Not Allowed
触发场景: 向未定义的路由发送请求时
解决方案:
- 检查路由定义方法(GET/POST等)是否匹配
- 验证请求URL末尾是否误加斜杠
- 使用APIRouter时检查前缀配置
4.3 500 Internal Server Error
排查步骤:
- 查看uvicorn日志定位异常堆栈
- 在代码中添加中间件捕获异常:
python
@app.middleware("http")
async def catch_exceptions(request, call_next):
try:
return await call_next(request)
except Exception as exc:
logger.error(f"Unhandled exception: {exc}")
return JSONResponse(...)
- 使用测试覆盖率工具检查边缘用例