第 4 章 · 04 组织测试与标记 mark
本章目标:学会用类和模块合理组织测试,用内置标记(skip/xfail)处理"暂时跑不了"的场景,并用自定义标记给测试分类打标签。
4.1 用类组织测试
测试多了以后,同一功能的用例应该归拢。pytest 支持把测试放进 Test 前缀的类中,规则只有两条:
- 类名必须以
Test开头,且不能定义__init__方法(否则整个类被跳过收集); - 测试方法以
test_开头,不需要继承任何基类。
# tests/test_cart.py
class TestAddItem:
"""购物车添加商品相关用例"""
def test_add_new_item(self):
cart = Cart()
cart.add("apple", qty=2)
assert cart.count("apple") == 2
def test_add_same_item_merges(self):
cart = Cart()
cart.add("apple", qty=1)
cart.add("apple", qty=3)
assert cart.count("apple") == 4
class TestRemoveItem:
def test_remove_existing(self):
...为什么禁止 __init__
pytest 的 fixture 机制(第 6 章起)取代了"构造函数初始化"的传统做法;带 __init__ 的 Test 类会被 pytest 静默跳过,这是新手最常见的"我的测试怎么没被跑到"原因之一。
模块级组织上,官方推荐两种布局之一:测试与源码分离(src/mypkg/ + 顶层 tests/),并配合可编辑安装(pip install -e .)。新项目建议在配置里启用 --import-mode=importlib 以避免同名测试文件冲突。
4.2 内置标记:skip 与 skipif
**标记(mark)**是打在测试函数上的元数据。最常用的一组是"跳过家族":
import sys
import pytest
@pytest.mark.skip(reason="功能尚未实现,先跳过")
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="仅支持 POSIX 平台")
def test_posix_paths():
...
# 条件写在模块顶部变量里更易维护
SUPPORTS_ORJSON = True
@pytest.mark.skipif(not SUPPORTS_ORJSON, reason="需要 orjson")
def test_fast_json():
...无条件跳过用 skip;条件跳过用 skipif(cond, reason=...)。被跳过的测试在报告中显示为 s,并会附上你写的 reason——永远写清楚原因,半年后没人记得为什么跳。
运行时动态跳过也有对应工具 pytest.skip(),常用于 fixture 内部检测到环境不满足时提前放弃。
4.3 内置标记:xfail——预期失败
有些用例"现在就该失败但将来会好",比如依赖一个还没合并的 bugfix。硬跳过会掩盖它真正变好的时刻,xfail 则让 pytest 持续执行它:
@pytest.mark.xfail(reason="等待上游库修复 #123")
def test_upstream_bug():
result = buggy_function()
assert result.correct
@pytest.mark.xfail(strict=True, reason="严格模式:一旦通过就让测试失败")
def test_will_be_fixed_soon():
...结果语义值得专门记住:
- 测试失败 →
xfailed(报告中的x):符合预期; - 测试意外通过 →
xpassed(报告中的X); strict=True时,"意外通过"会被判定为失败,逼你在问题修复后立刻摘掉这个标记。
4.4 自定义标记与注册
自定义标记就是一个任意的装饰器,用来给测试贴业务标签:
# tests/test_payment.py
import pytest
@pytest.mark.slow
@pytest.mark.payment
def test_full_refund_flow():
...未注册的标记每次使用都会产生 PytestUnknownMarkWarning 警告——这是防止你手滑写错标记名的保护机制。正确做法是在配置文件中注册(第 11 章详解配置文件本身):
# pyproject.toml
[pytest]
markers = [
"slow: 标记慢速测试,日常开发用 -m 'not slow' 排除",
"payment: 支付领域用例",
]要更严格,可以加 --strict-markers(或配置 strict_markers = true):此时使用未注册的标记直接报错而非警告,推荐所有正式项目开启。
pytest --markers # 列出所有已注册标记及说明
pytest -m slow # 只跑慢速测试
pytest -m "not slow" # 排除慢速测试4.5 标记的叠加与作用范围
多个标记可以叠加;标记也可以加在整个类或模块上:
# 类级标记:类内所有测试都生效
@pytest.mark.slow
class TestEtlPipeline:
def test_extract(self):
...
def test_load(self):
...
# 模块级标记:赋值给约定俗成的全局变量 pytestmark
pytestmark = pytest.mark.skipif(sys.version_info < (3, 10), reason="需要 Python 3.10+")注意两点边界:
- 标记只能作用于测试,对 fixture 没有效果;
-m表达式支持and / or / not组合,如pytest -m "payment and not slow"可以精确圈定本次要跑的范围——这是大型项目日常开发提速的核心手段。
4.6 本章小结
Test前缀、无__init__的类用于分组测试;测试与源码分离布局 + importlib 导入模式是新项目推荐姿势;skip(reason=...)无条件跳过,skipif(cond, reason=...)条件跳过,报告显示s;xfail表示"预期失败":失败得xfailed,意外通过得xpassed,strict=True把意外通过升级为失败;- 自定义标记必须在配置中注册(或开
--strict-markers),否则有警告/报错; -m "not slow"是日常开发的效率利器。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 以下哪个类的 test_ 方法不会被 pytest 收集?
2. @pytest.mark.xfail(strict=True) 的测试意外通过了,结果是?
3. 使用了未注册的自定义标记 @pytest.mark.slwo(拼写错误),默认会发生什么?
4. 想"只跑支付相关的、排除慢速的"测试,正确的命令是?
🛠️ 动手实践
- 给你的项目添加
slow和integration两个自定义标记并在pyproject.toml注册,然后验证pytest -m "not slow"能排除它们。 - 写一个依赖当前时间的用例,用
skipif让它在周末自动跳过(提示:datetime.date.today().weekday() >= 5)。 - 找一个已知因环境缺失而失败的用例改造成
xfail(strict=True),再人为补齐依赖让它通过,观察xpassed→FAILED的强制提醒效果。
会组织、会打标之后,下一章进入 pytest 提升测试密度的大杀器:参数化。