第 5 章 · 05 参数化测试
本章目标:用
@pytest.mark.parametrize把"同一逻辑、多组数据"的测试压缩成一份代码,并掌握多参数、笛卡尔积、自定义用例 ID 和pytest.param这些进阶写法。
5.1 为什么需要参数化
验证 is_palindrome,笨办法是复制粘贴十个测试函数;参数化的办法是一份测试逻辑 + 一张数据表:
import pytest
@pytest.mark.parametrize(
"text, expected",
[
("level", True),
("hello", False),
("", True),
("A man, a plan, a canal: Panama", True),
],
)
def test_is_palindrome(text, expected):
from strings import is_palindrome
assert is_palindrome(text) == expected运行时 pytest 会为每行数据生成一个独立用例,节点 ID 形如 test_is_palindrome[level]、test_is_palindrome[hello]。每条数据独立通过/失败——一条挂了不影响其他数据的验证,这正是参数化优于循环 for 断言的地方:
def test_bad_style():
# 反模式:循环里断言,第一条失败就中断,后面的数据根本没被测到
for text, expected in [("level", True), ("hello", False)]:
assert is_palindrome(text) == expected参数值不会被复制
官方文档特别提醒:参数值按原样传入测试。如果你传了列表或字典并在用例里修改了它,修改会影响到后续用例——数据表里放可变对象时务必小心(或每条数据用工厂函数生成新对象)。
5.2 失败时的可读性:ids 与 pytest.param
默认的用例 ID 由参数值直接拼成,遇到长字符串或对象会很难看。两种改进方式:
@pytest.mark.parametrize(
"text, expected",
[
pytest.param("level", True, id="简单回文"),
pytest.param("hello", False, id="普通单词"),
pytest.param("", True, id="空字符串"),
pytest.param("A man, a plan, a canal: Panama", True, id="经典长句"),
],
ids=None, # 每条用 pytest.param 的 id 覆盖
)
def test_is_palindrome_named(text, expected):
...也可以给一个 ids 列表或函数,按参数自动生成:
def make_id(val):
if isinstance(val, str):
return val[:10]
return str(val)
@pytest.mark.parametrize("text", ["apple", "banana"], ids=make_id)
def test_lower(text):
assert text.lower() == textpytest.param 还能给单条数据叠加标记——这是"个别数据已知有问题"时的标准解法:
@pytest.mark.parametrize(
"n, expected",
[
(1, 2),
pytest.param(6 * 9, 42, marks=pytest.mark.xfail(reason="银河系搭车客梗"), id="深思考"),
pytest.param(99, 100, marks=pytest.mark.slow),
],
)
def test_increment(n, expected):
assert n + 1 == expected5.3 多参数与笛卡尔积
parametrize 天然支持多个参数名,数据按位置对应:
@pytest.mark.parametrize(
"a, b, expected",
[
(2, 3, 5),
(-1, -2, -3),
(0, 0, 0),
],
)
def test_add(a, b, expected):
assert a + b == expected把装饰器叠加起来就得到笛卡尔积——每个参数各取所有值,组合数是相乘:
@pytest.mark.parametrize("x", [1, 10])
@pytest.mark.parametrize("y", [2, 5])
def test_multiply(x, y):
# 生成 4 条用例: [2-1] [2-10] [5-1] [5-10]
assert x * y > 0叠加时下面的装饰器变化更快。笛卡尔积适合验证"任意组合都不崩"这类性质,但组合数容易失控(3×3×3=27 条),要克制使用。
5.4 类级与模块级参数化
parametrize 标记可以放在类上,类内所有测试都会收到参数;放在模块级则赋给约定变量 pytestmark:
@pytest.mark.parametrize("n, expected", [(1, 2), (3, 4)])
class TestIncrement:
def test_simple(self, n, expected):
assert n + 1 == expected
def test_weird(self, n, expected):
assert (n * 1) + 1 == expected# 模块级参数化:本文件所有测试都收到 n/expected
import pytest
pytestmark = pytest.mark.parametrize("n, expected", [(1, 2), (3, 4)])5.5 何时参数化,何时拆分
判断标准是失败语义:
- 多组数据验证同一个行为规则 → 参数化(失败时你只关心哪条数据错了);
- 不同行为/不同异常路径 → 拆成独立测试函数(失败时你关心的是哪个行为坏了)。
另外 pytest 9 还提供 subtests 能力作为参数化的替代方案(适合"循环内软断言、失败不中断"的场景),可在官方文档 How to use subtests(如何使用 subtests)一节了解。
5.6 本章小结
@pytest.mark.parametrize("a,b", [(..), ..])一份逻辑 × 数据表 = N 条独立用例,优于循环断言;- 用例 ID 可用
pytest.param(id=...)或ids=函数定制,非 ASCII 默认会被转义; pytest.param(marks=...)给单条数据叠加 skip/xfail 等标记;- 叠加多个装饰器得到笛卡尔积,注意组合数爆炸;
- 参数值按引用传入,可变对象被修改会影响后续用例。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 参数化与在测试函数里 for 循环断言相比,核心优势是?
2. 叠加两个 @pytest.mark.parametrize(分别 2 个值和 3 个值),会生成多少条用例?
3. 想给参数化数据表中的某一条数据单独打上 xfail 标记,应该?
4. 关于参数化传入可变对象(如 list),官方提醒的正确理解是?
🛠️ 动手实践
- 为
parse_iso_date(text) -> datetime.date编写参数化测试:合法输入 3 条、非法输入(抛ValueError)2 条,非法组用pytest.raises并配id。 - 用两个叠加的
parametrize验证abs(x * y)对 x∈{-1, 0, 1}、y∈{2, 3} 恒不小于 0,先在纸上写出 6 个节点 ID 再运行核对。 - 把第 3 章"多断言聚合"的那个 schema 测试改造成参数化版本:每条数据是一个"坏字段 + 期望错误信息"。
数据驱动已就绪,下一章开始学习 pytest 最核心的依赖注入机制:fixture。