云计算百科
云计算领域专业知识百科平台

pytest 参数化与 Mark 标记

上接:[《pytest 断言详解与 Fixture 入门》]
适读人群:已掌握 Fixture 基础,想写出"一组代码、多组数据"的同学
环境要求:Python ≥ 3.10,pytest ≥ 8.0(推荐 9.x)
预计阅读:16 分钟


前言

第 2 篇我们解决了"怎么准备测试数据"(Fixture)。但马上会遇到一个新痛点:

同一个 add 函数,我想测 (1,2)、(-1,1)、(0,0)、(100,200)……难道要复制粘贴 20 个 test_add_xxx?

答案显然是否定的。pytest 提供了两个杀手级特性来解决这个问题:

  • ✅ 参数化(Parametrize):一组数据驱动,自动生成 N 个用例
  • ✅ Mark 标记:给用例打标签,按需分组执行
  • 读完本篇,你就能把"复制粘贴的测试"重构为简洁、可维护、可分组的专业测试套件。


    一、痛点:复制粘贴的测试

    先看一段"真实但不优雅"的代码:

    # tests/test_calc.py
    from src.calculator import add

    def test_add_positive():
    assert add(1, 2) == 3

    def test_add_negative():
    assert add(1, 1) == 0

    def test_add_zero():
    assert add(0, 0) == 0

    问题很明显:

    • 🔴 重复:每个用例只有数据不同,结构完全一样
    • 🔴 难扩展:新增一组边界数据就要再加一个函数
    • 🔴 难维护:逻辑一变要改 N 处

    这正是参数化的用武之地。


    二、参数化基础:@pytest.mark.parametrize

    2.1 最简用法

    把上面的三个用例合三为一:

    import pytest
    from src.calculator import add

    @pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (1, 1, 0),
    (0, 0, 0),
    ])
    def test_add(a, b, expected):
    assert add(a, b) == expected

    运行 pytest -v:

    tests/test_calc.py::test_add[1-2-3] PASSED [ 33%]
    tests/test_calc.py::test_add[-1-1-0] PASSED [ 66%]
    tests/test_calc.py::test_add[0-0-0] PASSED [100%]

    ========================= 3 passed in 0.08s =========================

    🎯 关键观察:

    • 一个函数 → 自动变成 3 个用例
    • 用例名后带 [参数] 后缀,一眼看出是哪组数据
    • 哪组失败,直接定位到具体参数组合

    2.2 参数拆解

    @pytest.mark.parametrize("参数名1, 参数名2", [ # ← 参数名字符串,逗号分隔
    (1,2), # ← 第一组
    (1,2), # ← 第二组
    ])

    要素说明
    第一个参数(字符串) 用逗号分隔的形参名,会注入到测试函数参数里
    第二个参数(列表) 一组或多组实参元组
    元组长度 必须与形参名数量一致

    ⚠️ 常见错误:形参名写 "a, b" 但元组只给一个值 (3,) —— 会报 ValueError: not enough values。


    三、参数化进阶

    3.1 给每组数据起名字(ids)

    默认用例名是 [1-2-3] 这种,可读性差。用 ids 改善:

    @pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (1, 1, 0),
    (0, 0, 0),
    ], ids=["正数", "异号", "零值"])
    def test_add(a, b, expected):
    assert add(a, b) == expected

    运行显示:

    test_add[正数] PASSED
    test_add[异号] PASSED
    test_add[零值] PASSED

    ✅ 强烈推荐:用例报告里一眼看懂场景,排查更快。

    3.2 参数用 pytest.param + mark

    想给某组参数单独打标记(比如标记某组为"预期失败"):

    import pytest
    from src.calculator import div

    @pytest.mark.parametrize("a, b, expected", [
    pytest.param(6, 2, 3, id="正常除法"),
    pytest.param(5, 2, 2.5, id="有余数"),
    pytest.param(1, 0, None, id="除零", marks=pytest.mark.xfail),
    ])
    def test_div(a, b, expected):
    assert div(a, b) == expected

    这里 pytest.param(…, marks=…) 给单组数据加 mark,xfail 表示"预期失败",pytest 会把它单独归类。

    3.3 参数组合(笛卡尔积)

    多个 parametrize 叠加 = 所有组合:

    @pytest.mark.parametrize("x", [1, 2])
    @pytest.mark.parametrize("y", ["a", "b"])
    def test_combo(x, y):
    print(f"{x}{y}")

    会生成 2 × 2 = 4 个用例:1-a、1-b、2-a、2-b。

    ⚠️ 组合爆炸要小心:3 组各 10 个参数 = 1000 个用例,运行会很慢。

    3.4 参数化 + Fixture 结合

    参数作为 Fixture 的输入,更灵活:

    @pytest.fixture
    def user_data(request):
    return request.param # ← 接收 parametrize 传入的值

    @pytest.mark.parametrize("user_data", [
    {"name": "alice", "age": 20},
    {"name": "bob", "age": 30},
    ], indirect=True) # ← 关键:把参数交给同名 fixture
    def test_user(user_data):
    assert user_data["name"]

    indirect=True 表示"这个参数不直接传给函数,而是传给同名 fixture"。适合需要在 fixture 里做预处理(如连接数据库)的场景。


    四、参数化最佳实践速查

    场景推荐做法
    多组数据测同一逻辑 @parametrize + ids
    某组数据预期失败 pytest.param(…, marks=pytest.mark.xfail)
    全组合覆盖 多个 parametrize 叠加(注意爆炸)
    参数需预处理 indirect=True + fixture
    大量数据(上百组) 数据放 YAML/JSON,循环生成

    五、什么是 Mark?为什么需要它?

    参数化解决"数据重复",Mark 解决"用例分组"。

    现实项目中用例成千上万:

    • 冒烟测试(smoke)只要跑核心 10 个
    • 回归测试(regression)才跑全部
    • 慢速的集成测试只想在 CI 夜间跑

    👉 Mark 就是给用例贴标签,运行时按标签筛选。


    六、内置 Mark 速览

    Mark作用
    @pytest.mark.skip 无条件跳过
    @pytest.mark.skipif 条件满足时跳过
    @pytest.mark.xfail 预期失败(失败不算报错)
    @pytest.mark.parametrize 参数化(上节已讲)
    @pytest.mark.usefixtures 声明使用的 fixture

    示例:

    import sys
    import pytest

    @pytest.mark.skip(reason="功能未实现")
    def test_todo():
    ...

    @pytest.mark.skipif(sys.version_info < (3, 12), reason="需要 3.12+")
    def test_new_feature():
    ...

    @pytest.mark.xfail
    def test_known_bug():
    assert 1 == 2 # 预期失败,报 XFAIL 而不是 FAILED


    七、自定义 Mark:按业务分组

    7.1 定义 mark

    在 pytest.ini 里注册(pytest 9.x 强制要求,否则报 PytestUnknownMarkWarning):

    [pytest]
    markers =
    smoke: 冒烟测试,核心链路
    regression: 回归测试
    slow: 运行较慢的集成测试
    api: 接口相关用例

    7.2 使用 mark

    import pytest
    from src.calculator import add

    @pytest.mark.smoke
    def test_add_core():
    assert add(1, 1) == 2

    @pytest.mark.regression
    @pytest.mark.slow
    def test_add_boundary():
    assert add(999, 999) == 0

    7.3 按 mark 运行

    # 只跑冒烟
    pytest -m smoke

    # 跑冒烟 + 回归(OR)
    pytest -m "smoke or regression"

    # 跑回归但不跑慢的(AND + NOT)
    pytest -m "regression and not slow"

    ✅ 这让你的 CI 流水线可以这样设计:

    • 提交代码 → 跑 -m smoke(快,分钟级)
    • 合并到主干 → 跑全量回归

    八、Mark 进阶技巧

    8.1 类级别 / 模块级别 mark

    # 整个类的用例都带 mark
    @pytest.mark.api
    class TestUserAPI:
    def test_get(self): ...
    def test_post(self): ...

    # 整个模块统一打 mark(写在文件顶部)
    pytestmark = pytest.mark.regression

    8.2 动态添加 mark

    def test_dynamic(request):
    if some_condition:
    request.node.add_marker(pytest.mark.slow)


    九、参数化 + Mark 组合实战

    把两者结合,才是真实项目常见写法:

    import pytest
    from src.calculator import add

    @pytest.mark.parametrize("a, b, expected", [
    pytest.param(1, 1, 2, id="core", marks=pytest.mark.smoke),
    pytest.param(100, 200, 300, id="big", marks=pytest.mark.regression),
    pytest.param(1, 1, 2, id="negative", marks=pytest.mark.regression),
    ], ids=["core", "big", "negative"])
    @pytest.mark.smoke
    @pytest.mark.regression
    def test_add(a, b, expected):
    assert add(a, b) == expected

    • 数据组层面:每组可单独标 smoke / regression
    • 函数层面:整体再叠加标签

    十、项目结构(落地推荐)

    沿用前两篇的 src/ + tests/ 结构,本篇新增配置:

    pytest-demo/
    ├── src/
    │ └── calculator.py
    ├── tests/
    │ ├── conftest.py
    │ └── test_calc.py
    ├── pytest.ini
    └── requirements.txt

    pytest.ini 更新版:

    [pytest]
    testpaths = ["tests"]
    python_files = test_*.py
    python_classes = Test*
    python_functions = test_*
    addopts = -v –tb=short

    markers =
    smoke: 冒烟测试
    regression: 回归测试
    slow: 慢速测试
    api: 接口相关


    十一、完整运行效果示例

    $ pytest -v
    ============================= test session starts ==============================
    collected 8 items

    tests/test_calc.py::test_add[正数] PASSED [ 12%]
    tests/test_calc.py::test_add[异号] PASSED [ 25%]
    tests/test_calc.py::test_add[零值] PASSED [ 37%]
    tests/test_calc.py::test_div[正常除法] PASSED [ 50%]
    tests/test_calc.py::test_div[有余数] PASSED [ 62%]
    tests/test_calc.py::test_div[除零] XFAIL [ 75%]
    tests/test_calc.py::test_add_core PASSED [ 87%]
    tests/test_calc.py::test_add_boundary PASSED [100%]

    ==================== 7 passed, 1 xfailed in 0.11s ==========================

    注意 XFAIL:标记为 xfail 的用例失败是符合预期的,不算报错 —— 这正是 mark 的价值。


    十二、参数化 + Mark 决策图

    下面这张图帮你判断"什么时候用什么":

    在这里插入图片描述

    核心判断:

    • 痛点 = “同一逻辑多组数据” → 参数化
    • 痛点 = “只想跑一部分用例” → Mark 分组
    • 两者经常叠加使用

    十三、常见坑与排错

    问题原因解决
    PytestUnknownMarkWarning mark 未注册 在 pytest.ini 的 markers 里注册
    参数元组长度不匹配 形参与实参数量不一致 检查 parametrize 的字符串与元组
    组合爆炸跑太慢 多个 parametrize 叠加 精简数据或用 fixture 动态生成
    -m 没筛到用例 mark 名拼写错误 pytest –markers 查看已注册 mark
    ids 重复 多组数据用了相同 id 保证 ids 唯一

    十四、小结

    ✅ 参数化:一组数据驱动多个用例,消灭复制粘贴
    ✅ ids:给每组数据起名,报告更易读
    ✅ Mark:给用例打标签,实现按需分组执行
    ✅ 参数化 + Mark:真实项目的标准组合
    ✅ 务必在 pytest.ini 注册自定义 mark,避免告警


    十五、思考题(欢迎评论区讨论)

  • 什么场景下"参数组合笛卡尔积"会变成反模式?如何规避?
  • indirect=True 的参数化解决了什么问题?能举个例子吗?
  • 你所在项目的 CI 是如何划分 smoke / regression 的?

  • 实战练习:动手试试

    在你本地第 2 篇的项目基础上:

  • 给 calculator.py 的 mul 函数写参数化测试,至少覆盖 5 组数据并用 ids 命名
  • 把其中一组标记为 pytest.mark.xfail(比如故意写错 expected)
  • 在 pytest.ini 注册 smoke / regression,用 -m 分别运行两组
  • 运行 pytest -v,观察 XFAIL 与 PASSED 的区别
  • 完成后欢迎在评论区贴出你的 parametrize 写法,我们一起交流 👇


    下一篇预告

    👉 《pytest Fixture 进阶:conftest 与依赖注入》

    你将学到:

    • conftest.py 的层级与作用域
    • Fixture 之间的依赖组合
    • 如何用 Fixture 实现"测试数据工厂"


    附录 A:参数化速查

    写法用途
    @parametrize("a", [1,2,3]) 单参数
    @parametrize("a,b", [(1,2),(3,4)]) 多参数
    ids=[…] 给每组起名
    pytest.param(…, marks=…) 单组加 mark
    多个 @parametrize 叠加 笛卡尔积
    indirect=True 参数交给 fixture

    附录 B:常用 mark 筛选命令

    pytest -m smoke # 只跑冒烟
    pytest -m "smoke or regression" # 并集
    pytest -m "regression and not slow" # 排除
    pytest –markers # 查看所有已注册 mark

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » pytest 参数化与 Mark 标记
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!