Skip to content

第 4 章 · 04 组织测试与标记 mark

本章目标:学会用类和模块合理组织测试,用内置标记(skip/xfail)处理"暂时跑不了"的场景,并用自定义标记给测试分类打标签。

4.1 用类组织测试

测试多了以后,同一功能的用例应该归拢。pytest 支持把测试放进 Test 前缀的类中,规则只有两条:

  • 类名必须以 Test 开头,且不能定义 __init__ 方法(否则整个类被跳过收集);
  • 测试方法以 test_ 开头,不需要继承任何基类。
python
# 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)**是打在测试函数上的元数据。最常用的一组是"跳过家族":

python
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 持续执行它:

python
@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 自定义标记与注册

自定义标记就是一个任意的装饰器,用来给测试贴业务标签:

python
# tests/test_payment.py
import pytest


@pytest.mark.slow
@pytest.mark.payment
def test_full_refund_flow():
    ...

未注册的标记每次使用都会产生 PytestUnknownMarkWarning 警告——这是防止你手滑写错标记名的保护机制。正确做法是在配置文件中注册(第 11 章详解配置文件本身):

toml
# pyproject.toml
[pytest]
markers = [
    "slow: 标记慢速测试,日常开发用 -m 'not slow' 排除",
    "payment: 支付领域用例",
]

要更严格,可以加 --strict-markers(或配置 strict_markers = true):此时使用未注册的标记直接报错而非警告,推荐所有正式项目开启。

bash
pytest --markers        # 列出所有已注册标记及说明
pytest -m slow          # 只跑慢速测试
pytest -m "not slow"    # 排除慢速测试

4.5 标记的叠加与作用范围

多个标记可以叠加;标记也可以加在整个类或模块上:

python
# 类级标记:类内所有测试都生效
@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,意外通过得 xpassedstrict=True 把意外通过升级为失败;
  • 自定义标记必须在配置中注册(或开 --strict-markers),否则有警告/报错;
  • -m "not slow" 是日常开发的效率利器。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 以下哪个类的 test_ 方法不会被 pytest 收集?

2. @pytest.mark.xfail(strict=True) 的测试意外通过了,结果是?

3. 使用了未注册的自定义标记 @pytest.mark.slwo(拼写错误),默认会发生什么?

4. 想"只跑支付相关的、排除慢速的"测试,正确的命令是?

🛠️ 动手实践

  1. 给你的项目添加 slowintegration 两个自定义标记并在 pyproject.toml 注册,然后验证 pytest -m "not slow" 能排除它们。
  2. 写一个依赖当前时间的用例,用 skipif 让它在周末自动跳过(提示:datetime.date.today().weekday() >= 5)。
  3. 找一个已知因环境缺失而失败的用例改造成 xfail(strict=True),再人为补齐依赖让它通过,观察 xpassed→FAILED 的强制提醒效果。

会组织、会打标之后,下一章进入 pytest 提升测试密度的大杀器:参数化。