| name | python-pytest |
| description | 使用 pytest 编写高质量测试的通用规范。涵盖单元测试、集成测试(内部流程)、生产环境冒烟测试、 测试目录三层分离、fixture 使用、Mock 策略、参数化测试、异步测试和覆盖率目标。 在创建或修改测试文件时使用此 skill。
|
Python pytest 测试规范
核心原则
- 测试应当快速、独立、可重复、自验证(FIRST 原则)
- 每个测试函数只验证一个行为
- 测试名称应清晰表达被测行为和预期结果
- 目标测试覆盖率:核心逻辑 ≥ 80%
测试类型决策树
| 要测试什么 | 推荐方式 |
|---|
| 函数/方法的返回值 | 基本单元测试,AAA 模式 |
| 函数的行为(调用、副作用) | Mock 验证函数调用和参数 |
| 数据库/API 等外部依赖 | Fixtures + Mock 外部服务 |
| 异步代码 | pytest-asyncio,配置 asyncio_mode = "auto" |
| 多种输入组合 | @pytest.mark.parametrize 参数化测试 |
| 异常情况 | pytest.raises |
| 共享测试数据或资源 | Fixtures(按作用域选择) |
目录结构
project/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── calculator.py
│ └── user_service.py
└── tests/
├── conftest.py # 共享 fixture
├── unit/ # 单元测试(快,无外部依赖)
├── integration/ # 集成测试(测试环境,连真实依赖)
└── production/ # 生产冒烟(只读、健康检查)
注意:tests/ 目录不需要 __init__.py 文件。
按层执行:
pytest tests/unit/
pytest tests/integration/
pytest tests/production/ -v
pytest -m "not production"
测试文件命名
- 测试文件以
test_ 开头:test_calculator.py
- 测试函数以
test_ 开头:def test_add_returns_correct_sum()
- 辅助文件不使用
test_ 前缀:helpers.py、factories.py
测试函数命名规范
遵循 test_<功能>_<场景>_<预期结果> 模式:
def test_divide_by_zero_raises_value_error():
def test_create_user_with_valid_data_returns_user():
def test_login_with_invalid_password_returns_401():
def test_divide():
def test_user():
def test_login2():
基本测试结构(AAA 模式)
def test_calculate_discount_returns_correct_price():
price = 100.0
rate = 0.2
result = calculate_discount(price, rate)
assert result == 80.0
使用 pytest fixture
定义 fixture
import pytest
from mypackage.database import Database
@pytest.fixture
def db():
"""提供测试数据库连接,测试结束后自动清理。"""
database = Database(":memory:")
database.create_tables()
yield database
database.close()
@pytest.fixture
def sample_user():
return {"id": 1, "name": "张三", "email": "zhang@example.com"}
使用 fixture
def test_save_user_persists_to_database(db, sample_user):
db.save_user(sample_user)
retrieved = db.get_user(sample_user["id"])
assert retrieved["name"] == sample_user["name"]
fixture 作用域
@pytest.fixture(scope="session")
@pytest.fixture(scope="module")
@pytest.fixture(scope="class")
@pytest.fixture(scope="function")
参数化测试
使用 @pytest.mark.parametrize 避免重复测试代码:
import pytest
@pytest.mark.parametrize("price,rate,expected", [
(100.0, 0.0, 100.0),
(100.0, 0.5, 50.0),
(100.0, 1.0, 0.0),
(0.0, 0.3, 0.0),
])
def test_calculate_discount(price, rate, expected):
assert calculate_discount(price, rate) == expected
@pytest.mark.parametrize("invalid_rate", [-0.1, 1.1, 2.0])
def test_calculate_discount_with_invalid_rate_raises(invalid_rate):
with pytest.raises(ValueError, match="折扣率必须在"):
calculate_discount(100.0, invalid_rate)
异常测试
def test_divide_by_zero_raises_zero_division_error():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_invalid_email_raises_with_message():
with pytest.raises(ValueError, match="邮箱格式不正确"):
validate_email("not-an-email")
def test_custom_exception_has_correct_code():
with pytest.raises(AppError) as exc_info:
raise_app_error(code=404)
assert exc_info.value.code == 404
Mock 和 Patch
monkeypatch vs pytest-mock
monkeypatch — 简单替换,适合修改环境变量、配置等:
def test_with_monkeypatch(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key")
monkeypatch.setattr("module.function", mock_function)
pytest-mock (mocker) — 强大的 mock 功能,适合验证调用:
def test_with_mocker(mocker):
mock_func = mocker.patch("module.function")
mock_func.return_value = "result"
mock_func.assert_called_with("args")
Patch 策略
✅ 正确:在使用的地方 patch(patch 被测试代码所引用的路径)
mocker.patch("myapp.utils.helper.function")
❌ 错误:在定义的地方 patch
from myapp.utils import helper
mocker.patch("helper.function")
常见 Mock 场景
from unittest.mock import MagicMock, patch
def test_send_email_calls_smtp_once():
with patch("mypackage.email.smtplib.SMTP") as mock_smtp:
mock_instance = mock_smtp.return_value.__enter__.return_value
send_welcome_email("user@example.com")
mock_instance.sendmail.assert_called_once()
def test_get_user_calls_api_with_correct_id():
mock_client = MagicMock()
mock_client.get.return_value = {"id": 1, "name": "李四"}
service = UserService(client=mock_client)
user = service.get_user(1)
mock_client.get.assert_called_once_with("/users/1")
assert user["name"] == "李四"
集成测试(内部流程)
集成测试验证多模块串联的完整业务流程,与单元测试的核心区别在于 Mock 范围:
| 单元测试 | 集成测试 |
|---|
| Mock 范围 | Mock 一切外部依赖 | 只 Mock 真正的外部边界(DB、HTTP、文件) |
| 测试粒度 | 单个函数/方法 | 完整业务流程 |
| 速度 | 极快 | 较慢,可接受 |
只 Mock 外部边界,内部逻辑走真实代码
def test_factor_compute_pipeline(mocker, sample_data):
mocker.patch("myapp.data.get_klines", return_value=sample_data)
engine = TimeSeriesEngine(data=sample_data, factor=factor)
result = engine.results
assert not result.empty
assert "F#SMA60#DEFAULT" in result.columns
按流程阶段组织,而非按模块
def test_factor_compute_returns_valid_output(sample_data):
"""阶段1:因子计算"""
engine = TimeSeriesEngine(data=sample_data, factor=factor)
assert not engine.results.empty
def test_factor_eval_ic_is_reasonable(factor_results):
"""阶段2:因子评估"""
ic = calc_ic(factor_results)
assert abs(ic) > 0.01
def test_model_weights_sum_to_one(factor_results):
"""阶段3:策略建模"""
model = CSSorting(factor_results, factor="F#SMA60#DEFAULT", top_pct=0.2)
weights = model.run()
assert abs(weights["weight"].sum()) <= 1.0
fixture scope 选择
| 场景 | 推荐 scope |
|---|
| 生成测试数据(无状态) | module 或 session |
| 有状态的对象(如引擎实例) | function |
| 数据库连接 | session |
常见坑
- Mock 太多 → 等于没测:如果内部多个模块都被 Mock,流程没有真正串联
- 测试间隐式依赖:测试 B 不能依赖测试 A 的副作用,每个用例应能独立运行
生产环境测试原则
pytest 可以做生产环境验证,但必须严格区分场景。
可以做的(安全场景)
| 场景 | 说明 |
|---|
| 冒烟测试 | 发布后验证服务启动、关键接口可用 |
| 健康检查 | 验证环境变量、证书、依赖服务连通性 |
| 流量回放 | 复制生产流量对比新旧版本,不影响用户 |
| 性能基准 | 验证新版本不比旧版慢、无内存泄漏 |
绝对禁止
- 带写操作的用例(建数据、删数据、改状态)→ 污染生产库
- 高并发测试 → 打垮生产服务
- 普通单元测试直接在生产跑(很多单测会清表、Mock 外部依赖)
生产冒烟示例
@pytest.mark.production
def test_api_health(client):
resp = client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "ok"
@pytest.mark.production
def test_api_response_time(client):
import time
start = time.time()
client.get("/api/user/info")
assert time.time() - start < 0.3
生产测试强制规则:只读不写、禁止造脏数据、单线程串行、禁止 truncate/drop。
异步测试
import pytest
async def test_async_fetch_returns_data():
result = await fetch_data("https://api.example.com/data")
assert result is not None
async def test_async_operation_with_fixture(db):
await db.async_save({"key": "value"})
result = await db.async_get("key")
assert result == "value"
标记(Markers)
@pytest.mark.slow
def test_large_data_processing():
...
@pytest.mark.network
def test_external_api_integration():
...
@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="不支持 Windows")
def test_unix_only_feature():
...
在 pyproject.toml 中注册自定义 marker:
[tool.pytest.ini_options]
markers = [
"slow: 标记为慢速测试,可用 -m 'not slow' 跳过",
"network: 需要网络连接的集成测试",
]
pytest 配置(pyproject.toml)
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-v --tb=short"
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "**/__init__.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
运行命令
pytest
pytest tests/test_calculator.py
pytest tests/test_calculator.py::test_add_returns_correct_sum
pytest -m slow
pytest -m "not slow"
pytest --cov=src --cov-report=term-missing
pytest -x
pytest -n auto
工作流程
编写新测试的步骤
- 确定测试类型 — 参考上方决策树,选择单元测试、集成测试或异步测试
- 创建测试文件 —
test_<module>.py
- 编写测试函数 — 遵循 AAA 模式
- 添加必要的 fixtures — 复用测试数据和资源
- Mock 外部依赖 — 保持测试隔离和快速
- 验证覆盖率 —
pytest --cov
- 运行测试 —
pytest -v
诊断测试失败
- 查看详细输出 —
pytest -v 或 pytest -vv
- 打印调试信息 — 在测试中使用
print()(提交前删除)
- 运行单个测试 —
pytest tests/test_file.py::test_function
- 查看完整错误栈 —
pytest --tb=long
详细参考文档
本 skill 包含以下详细参考文档(位于 references/ 目录):
Fixtures 完全指南:基础 fixtures、作用域、参数化、依赖关系、conftest.py、内置 fixtures(tmp_path、capsys、caplog、monkeypatch)、Async fixtures
Mocking 和 Patching 完全指南:monkeypatch 用法、pytest-mock 使用、Mock 对象配置和验证、常见 mock 模式(API、数据库、文件系统)、Patch 策略、常见陷阱
异步测试完全指南:pytest-asyncio 配置(auto/strict 模式)、基本异步测试、异步 fixtures、Loop scope 控制、Mock 异步操作
常用测试模式和示例:测试组织、参数化测试、异常测试、标记和跳过、测试配置、数据驱动测试、集成测试模式、边界条件测试
测试最佳实践和规范:FIRST 原则、AAA/Given-When-Then 模式、覆盖率配置、Fixture/Mock/断言最佳实践、性能考虑、CI/CD 集成
最佳实践
- 只运行相关测试,而非整个测试套件;按模块或功能范围执行,节省时间
- 编写新测试前先阅读现有测试,理解项目的测试风格和惯用模式,保持一致性
- 用
print 辅助调试,完成后及时删除;不要将调试输出留在最终提交的代码中
- 提交前解决所有错误和警告;通过 ruff、basedpyright 检查后再提交
测试质量清单
在提交测试代码前确认: