测试策略实战
问题
Python 项目的测试策略应该如何设计?pytest 有哪些高级用法?
答案
测试金字塔
单元测试
tests/test_service.py
import pytest
from unittest.mock import AsyncMock, patch
class TestOrderService:
@pytest.fixture
def service(self):
return OrderService(db=MockDB(), cache=MockCache())
def test_create_order_success(self, service):
order = service.create_order(user_id=1, items=[{"id": 1, "qty": 2}])
assert order.status == "pending"
assert order.total > 0
def test_create_order_empty_items(self, service):
with pytest.raises(ValueError, match="items cannot be empty"):
service.create_order(user_id=1, items=[])
@pytest.mark.parametrize("discount,expected", [
(0, 100),
(0.1, 90),
(0.5, 50),
(1.0, 0),
])
def test_apply_discount(self, service, discount, expected):
result = service.apply_discount(100, discount)
assert result == expected
Mock 外部依赖
tests/test_api.py
from unittest.mock import patch, AsyncMock
@pytest.mark.asyncio
async def test_fetch_user_from_api():
mock_response = AsyncMock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_response.status_code = 200
with patch("httpx.AsyncClient.get", return_value=mock_response):
user = await user_service.get_user(1)
assert user.name == "Alice"
# 使用 pytest-mock(更简洁)
async def test_send_email(mocker):
mock_send = mocker.patch("services.email.send_email", return_value=True)
result = await notify_user(user_id=1, message="Hello")
mock_send.assert_called_once_with("user@example.com", "Hello")
assert result is True
Fixture 与工厂
tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
@pytest.fixture(scope="session")
def engine():
"""整个测试会话共享一个数据库"""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def db_session(engine):
"""每个测试用例独立事务"""
conn = engine.connect()
trans = conn.begin()
session = Session(bind=conn)
yield session
session.close()
trans.rollback() # 回滚,保持数据库干净
conn.close()
@pytest.fixture
def user_factory(db_session):
"""工厂 Fixture"""
def create_user(**kwargs):
defaults = {"name": "Test User", "email": "test@example.com"}
defaults.update(kwargs)
user = User(**defaults)
db_session.add(user)
db_session.commit()
return user
return create_user
FastAPI 集成测试
tests/test_api_integration.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.mark.asyncio
async def test_create_user(client):
response = await client.post("/users", json={"name": "Alice", "email": "a@b.com"})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Alice"
@pytest.mark.asyncio
async def test_get_user_not_found(client):
response = await client.get("/users/999")
assert response.status_code == 404
常见面试问题
Q1: pytest vs unittest?
答案:
| 特性 | pytest | unittest |
|---|---|---|
| 断言 | assert 原生 | self.assertEqual() |
| Fixture | 灵活、可组合 | setUp/tearDown |
| 参数化 | @pytest.mark.parametrize | 第三方库 |
| 插件 | 丰富(200+) | 较少 |
| 推荐 | 新项目首选 | 兼容旧代码 |
Q2: 什么时候用 Mock,什么时候不用?
答案:
- 用 Mock:外部 API、邮件服务、支付接口、文件系统
- 不用 Mock:纯逻辑函数、数据库(用内存 SQLite 替代)
- 原则:Mock 边界,不 Mock 内部逻辑
Q3: 覆盖率目标多少合适?
答案:
- 整体 80% 以上
- 核心业务逻辑 90%+
- 工具类/配置类 60%+ 即可
- 不要追求 100%,关注有意义的测试