一、测试环境配置与基础框架搭建
在 FastAPI 开发中,完善的测试环境和基础框架是保证代码质量和可维护性的关键。以下是具体实现步骤:
1.1 环境配置与依赖管理
使用 pipenv 或 poetry 管理虚拟环境和依赖:
# 安装 pipenv
pip install pipenv
# 创建虚拟环境并安装依赖
pipenv install fastapi uvicorn pytest httpx pydantic==2.0.0 sqlalchemy==2.0.0
依赖说明:
fastapi: Web 框架核心uvicorn: ASGI 服务器pytest: 测试框架httpx: 测试 HTTP 请求pydantic: 数据验证(v2.0 新特性支持严格类型校验)sqlalchemy: ORM 工具
1.2 基础框架结构
创建项目目录结构:
project/
├── app/
│ ├── main.py # 应用入口
│ ├── routes/ # API 路由
│ ├── models/ # Pydantic 数据模型
│ ├── database.py # 数据库连接
│ └── config.py # 配置文件
├── tests/
│ ├── conftest.py # 测试配置
│ └── test_api.py # API 测试用例
└── requirements.txt
1.3 核心框架代码
database.py (SQLAlchemy 配置):
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# 依赖注入数据库会话
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
config.py (Pydantic 配置管理):
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "FastAPI Demo"
debug_mode: bool = False
class Config:
env_file = ".env"
settings = Settings()
main.py (FastAPI 入口):
from fastapi import FastAPI, Depends
from .database import get_db
from .routes import items_router
from .config import settings
app = FastAPI(title=settings.app_name)
# 挂载路由
app.include_router(items_router, prefix="/items")
@app.get("/")
async def root():
return {"message": "Hello World"}
🔍 课后 Quiz 1
问题: 为什么使用 yield 而不是 return 提供数据库会话?
答案解析:
在 get_db 中使用 yield 实现依赖注入的生命周期管理:
yield前的代码在请求开始时执行(创建会话)yield后的代码在请求结束时执行(关闭会话)- 这种方式确保即使出现异常也能正确释放资源
⚠️ 常见报错解决方案 (1.X)
报错: 422 Unprocessable Entity
原因: 请求体不符合 Pydantic 模型定义
解决方案:
- 检查请求的 JSON 数据结构
- 验证模型字段是否匹配,例如:
class Item(BaseModel): name: str # 要求必须字符串类型 price: float - 使用
curl -v查看详细错误信息
预防建议:
- 为模型字段添加默认值,如
name: str = "default" - 使用
Union支持多类型,如price: Union[float, None] = None
二、测试覆盖率检测工具配置
测试覆盖率是衡量代码质量的核心指标。FastAPI 推荐使用:
- pytest:测试运行器
- coverage.py:覆盖率检测
- pytest-cov:集成插件
2.1 配置 pytest
tests/conftest.py (测试依赖注入):
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
2.2 编写测试用例
tests/test_api.py:
import pytest
# 测试 API 端点
@pytest.mark.asyncio
async def test_create_item(client):
response = await client.post(
"/items/",
json={"name": "Test Item", "price": 9.99} # 符合 Pydantic 模型
)
assert response.status_code == 200
assert response.json()["name"] == "Test Item"
# 测试无效数据
@pytest.mark.asyncio
async def test_invalid_item(client):
response = await client.post(
"/items/",
json={"price": "invalid"} # 缺少必要字段 name
)
assert response.status_code == 422 # 触发 Pydantic 验证错误
2.3 覆盖率检测配置
- 安装依赖:
pipenv install coverage pytest-cov - 运行测试并生成报告:
pytest --cov=app --cov-report=html tests/ - 查看 HTML 报告: open htmlcov/index.html
2.4 持续集成集成
在 .github/workflows/ci.yml 中配置:
name: CI Pipeline
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
- name: Install dependencies
run: pip install pipenv && pipenv install --dev
- name: Run tests
run: pytest --cov=app --cov-fail-under=80 # 要求覆盖率≥80%
🔍 课后 Quiz 2
问题: 覆盖率报告中 --cov-fail-under=80 参数的作用是什么?
答案解析:
该参数设置最低覆盖率阈值:
- 如果整体覆盖率低于 80%,测试将失败
- 防止未经充分测试的代码合并到主分支
- 在 CI/CD 流程中强制质量门禁
⚠️ 常见报错解决方案 (2.X)
报错: ModuleNotFoundError: No module named 'app'
原因: 测试运行路径错误
解决方案:
- 从项目根目录运行测试:
cd /project && pytest - 在
pytest.ini中添加:[pytest] pythonpath = .
预防建议:
- 使用
__init__.py将目录转为 Python 包 - 避免在测试中硬编码绝对路径
三、测试覆盖率优化策略
3.1 分支覆盖率测试
# 测试不同业务分支
@pytest.mark.parametrize("price, discount", [
(100, 10), # 正常折扣
(50, 0), # 无折扣
(30, -5) # 无效折扣
])
async def test_discount_logic(client, price, discount):
response = await client.post(
"/items/",
json={"name": "Test", "price": price, "discount": discount}
)
if discount < 0:
assert response.status_code == 400 # 验证业务规则
else:
assert response.status_code == 200
3.2 异步任务覆盖率
对于后台异步任务:
from fastapi import BackgroundTasks
async def notify_admins(email: str):
# 模拟发送邮件
print(f"Sending email to {email}")
@app.post("/report")
async def create_report(background_tasks: BackgroundTasks):
background_tasks.add_task(notify_admins, "admin@example.com")
return {"message": "Report scheduled"}
测试策略:
# Mock 后台任务
from unittest.mock import MagicMock
@pytest.mark.asyncio
async def test_background_task(client):
app.notify_admins = MagicMock() # 替换为 Mock 函数
response = await client.post("/report")
app.notify_admins.assert_called_once_with("admin@example.com")
3.3 目标覆盖率报告
----------- coverage: platform linux -----------
Name Stmts Miss Cover
-----------------------------------------
app/__init__.py 0 0 100%
app/main.py 15 0 100%
app/routes.py 20 1 95% # 缺失分支
-----------------------------------------
TOTAL 35 1 97%
graph TD
A[启动测试] --> B[pytest 执行用例]
B --> C{覆盖率采集}
C -->|SQLAlchemy| D[数据库操作]
C -->|Pydantic| E[数据验证]
C -->|BackgroundTasks| F[异步任务]
D --> G[生成报告]
E --> G
F --> G