跳到主要内容

测试策略实战

问题

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?

答案

特性pytestunittest
断言assert 原生self.assertEqual()
Fixture灵活、可组合setUp/tearDown
参数化@pytest.mark.parametrize第三方库
插件丰富(200+)较少
推荐新项目首选兼容旧代码

Q2: 什么时候用 Mock,什么时候不用?

答案

  • 用 Mock:外部 API、邮件服务、支付接口、文件系统
  • 不用 Mock:纯逻辑函数、数据库(用内存 SQLite 替代)
  • 原则:Mock 边界,不 Mock 内部逻辑

Q3: 覆盖率目标多少合适?

答案

  • 整体 80% 以上
  • 核心业务逻辑 90%+
  • 工具类/配置类 60%+ 即可
  • 不要追求 100%,关注有意义的测试

相关链接