#FastAPI Pytest单元测试完全指南
📂 所属阶段:第五阶段 — 工程化与部署(实战篇)
🔗 相关章节:FastAPI依赖注入系统 · FastAPI多环境配置
#单元测试基础概念
单元测试是软件开发中的基石,它确保代码的正确性、可维护性和可扩展性。
#为什么需要单元测试?
# 无测试 vs 有测试的开发体验
"""
无测试场景:
- 修改代码 → 手动测试 → 上线 → 发现bug → 回滚 → 用户投诉 😱
- 重构代码 → 担心破坏功能 → 代码腐化 → 技术债务堆积
有测试场景:
- 修改代码 → 运行测试 → 快速反馈 → 自信上线 🚀
- 重构代码 → 测试保证 → 持续优化 → 代码质量提升
"""#测试金字塔
在FastAPI应用中,遵循测试金字塔原则:
┌─────────────────────────┐ ← 单元测试 (Unit Tests) - 70%
│ 业务逻辑层 │ • 快速、隔离、专注
├─────────────────────────┤ ← 集成测试 (Integration) - 20%
│ API/路由层 │ • 测试API端点和数据流
├─────────────────────────┤ ← 端到端测试 (E2E) - 10%
│ UI/界面层 │ • 测试完整用户流程
└─────────────────────────┘#Pytest环境搭建
#核心依赖安装
# 基础测试依赖
pip install pytest pytest-asyncio httpx pytest-cov pytest-mock factory-boy faker#项目结构配置
# pyproject.toml - 现代Python项目配置
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
addopts = [
"-ra",
"--showlocals",
"--tb=short",
"--strict-markers",
"--strict-config",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
"api: marks tests as api tests",
]
[tool.coverage.run]
source = ["src/", "app/"]
omit = [
"*/venv/*",
"*/tests/*",
"*/migrations/*",
"*/__init__.py"
]#conftest.py配置文件
# tests/conftest.py - Pytest配置文件
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db, Base
TEST_DATABASE_URL = "sqlite:///./test.db"
@pytest.fixture(scope="function")
def test_engine():
"""创建测试数据库引擎"""
engine = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
yield engine
Base.metadata.drop_all(bind=engine)
engine.dispose()
@pytest.fixture(scope="function")
def test_session(test_engine):
"""创建测试数据库会话"""
Session = sessionmaker(bind=test_engine)
session = Session()
yield session
session.rollback()
session.close()
@pytest.fixture(scope="function")
def client(test_session):
"""同步测试客户端"""
def override_get_db():
try:
yield test_session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
@pytest.fixture(scope="function")
async def async_client(test_session):
"""异步测试客户端"""
def override_get_db():
try:
yield test_session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()#TestClient基础使用
#TestClient同步测试
# tests/test_basic.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
def test_root_endpoint(client):
"""测试根端点"""
response = client.get("/")
assert response.status_code == 200
assert "message" in response.json()
def test_post_request(client):
"""测试POST请求"""
test_data = {"name": "Test User", "email": "test@example.com"}
response = client.post("/users/", json=test_data)
assert response.status_code == 201
assert response.json()["name"] == "Test User"
def test_error_handling(client):
"""测试错误处理"""
response = client.get("/nonexistent-endpoint")
assert response.status_code == 404#异步测试详解
#异步测试基础
# tests/test_async.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_async_endpoint(async_client):
"""异步端点测试"""
response = await async_client.get("/")
assert response.status_code == 200
assert "message" in response.json()
@pytest.mark.asyncio
async def test_async_post(async_client):
"""异步POST请求测试"""
data = {"name": "Async User", "email": "async@example.com"}
response = await async_client.post("/users/", json=data)
assert response.status_code == 201#Fixtures与依赖注入测试
#Fixtures高级用法
# tests/test_fixtures.py
import pytest
from unittest.mock import AsyncMock, MagicMock
from app.services.user_service import UserService
@pytest.fixture
def mock_user_service():
"""模拟用户服务"""
mock = AsyncMock(spec=UserService)
mock.get_user_by_id.return_value = {
"id": 1,
"email": "mock@example.com",
"name": "Mock User"
}
return mock
@pytest.fixture
def sample_user_data():
"""样本用户数据"""
return {
"email": "test@example.com",
"password": "TestPassword123!",
"name": "Test User"
}
def test_with_mocked_service(mock_user_service):
"""使用模拟服务的测试"""
user = mock_user_service.get_user_by_id(1)
assert user["email"] == "mock@example.com"
def test_with_sample_data(client, sample_user_data):
"""使用样本数据的测试"""
response = client.post("/users/", json=sample_user_data)
assert response.status_code == 201
assert response.json()["email"] == sample_user_data["email"]#数据库测试策略
#数据库事务管理
# tests/test_database.py
import pytest
from sqlalchemy import text
from app.models import User
from app.schemas import UserCreate
class TestDatabase:
"""数据库测试"""
async def test_create_user(self, test_session):
"""测试创建用户"""
from app.crud import create_user
user_data = UserCreate(
email="test@example.com",
password="Password123!",
name="Test User"
)
user = create_user(test_session, user_data)
assert user.email == user_data.email
result = test_session.execute(
text("SELECT COUNT(*) FROM users WHERE email = :email"),
{"email": user_data.email}
)
count = result.scalar()
assert count == 1#API端点测试
#用户管理API测试
# tests/test_user_api.py
import pytest
class TestUserAPI:
"""用户API测试"""
@pytest.mark.asyncio
async def test_create_user(self, async_client):
"""测试创建用户"""
user_data = {
"email": "newuser@example.com",
"password": "SecurePassword123!",
"name": "New User"
}
response = await async_client.post("/users/", json=user_data)
assert response.status_code == 201
data = response.json()
assert data["email"] == user_data["email"]
assert data["name"] == user_data["name"]
@pytest.mark.asyncio
async def test_get_user(self, async_client):
"""测试获取用户"""
user_data = {
"email": "getuser@example.com",
"password": "Password123!",
"name": "Get User"
}
create_response = await async_client.post("/users/", json=user_data)
user_id = create_response.json()["id"]
response = await async_client.get(f"/users/{user_id}")
assert response.status_code == 200
assert response.json()["id"] == user_id#参数化测试与边界测试
#参数化测试
# tests/test_parameterized.py
import pytest
class TestParameterized:
"""参数化测试"""
@pytest.mark.parametrize("email,is_valid", [
("valid@example.com", True),
("user.name@example.com", True),
("invalid", False),
("@example.com", False),
])
@pytest.mark.asyncio
async def test_email_validation(self, async_client, email, is_valid):
"""参数化测试邮箱验证"""
user_data = {
"email": email,
"password": "Password123!",
"name": "Test User"
}
response = await async_client.post("/users/", json=user_data)
if is_valid:
assert response.status_code in [201, 409]
else:
assert response.status_code == 422#测试覆盖率分析
#覆盖率配置与执行
# 运行测试并生成覆盖率报告
pytest --cov=app --cov-report=html --cov-report=term-missing --cov-fail-under=80#详细的覆盖率配置
# .coveragerc - 覆盖率配置文件
[run]
source = app/
omit =
*/venv/*
*/tests/*
*/__init__.py
branch = True
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
show_missing = True#TDD开发实践
#TDD基础实践
"""
TDD (Test-Driven Development) 开发实践:
1. RED - 编写失败的测试
2. GREEN - 编写最少的代码使测试通过
3. REFACTOR - 重构代码,保持测试通过
"""
# 1. RED: 先写测试
class TestShoppingCartTDD:
"""购物车功能的TDD开发示例"""
def test_cart_starts_empty(self):
"""测试购物车初始化时空的 - RED"""
cart = ShoppingCart()
assert len(cart.items) == 0
assert cart.total_price == 0
# 2. GREEN: 实现最简单的代码使测试通过
class ShoppingCart:
def __init__(self):
self.items = []
@property
def total_price(self):
return sum(item["price"] * item["quantity"] for item in self.items)
def add_item(self, item):
self.items.append(item)
# 3. REFACTOR: 重构代码
class ShoppingCart:
def __init__(self):
self._items = {}
def add_item(self, item):
item_id = item["id"]
if item_id in self._items:
self._items[item_id]["quantity"] += item["quantity"]
else:
self._items[item_id] = item.copy()
@property
def items(self):
return list(self._items.values())
@property
def total_price(self):
return sum(item["price"] * item["quantity"]
for item in self._items.values())#总结
Pytest是Python中最强大的测试框架之一,特别适合FastAPI应用的测试:
- 简洁语法:使用简单的assert语句
- 强大Fixtures:灵活的依赖管理
- 参数化测试:轻松测试多种场景
- 丰富的插件:扩展功能强大
- 异步支持:天然支持async/await
💡 关键要点:良好的测试策略是应用质量的保障。Pytest提供了完整的测试解决方案,从单元测试到集成测试,从简单验证到复杂场景,都能胜任。

