ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python pytest装饰器总结(实例详解)

Python pytest装饰器总结(实例详解) 前言pytest是第三方测试框架需要单独安装pip install pytest标准库里没有它。它用到的「装饰器」其实分成两类混在一起讲就很容易乱pytest.fixture—— 定义夹具fixture也就是测试用的准备数据、临时资源。pytest.mark.xxx——标记mark例如parametrize、skip、skipif、xfail。官方文档把这些统称为 marks。有一个和普通装饰器很不一样的认知必须提前建立这些装饰器不是「用包装函数把测试函数包起来」那套运行时逻辑。它们主要是给 pytest 的收集阶段collection留下信息pytest 在运行测试前扫描模块、读取这些标记再决定怎么建夹具、怎么展开参数、要不要跳过。所以你不要按处理普通装饰器的思路给它们加functools.wraps也不需要理解它们返回什么。下面所有 API 名称都按 pytest 官方文档核对过但具体行为请以你所用版本的官方文档为准。本文示例按 pytest 7 以上、Python 3.8 以上书写。顺带一提现在的 pytest 早就不支持 Python 2 了而 Python 2 本身也已在 2020 年 1 月 1 日停止维护——看到print语句式的老测试代码别照抄。一、pytest.fixture夹具夹具的核心用法是测试函数只要把夹具名写成参数pytest 就会先调用夹具并把返回值传进来。# 适用于 Python 3.8import pytestpytest.fixturedef sample_data():return [1, 2, 3]def test_sum(sample_data):assert sum(sample_data) 6用yield做前后置清理——yield之前是准备之后是清理即使测试失败清理也会执行# 适用于 Python 3.8import pytestpytest.fixturedef temp_list():data []print(准备)yield dataprint(清理)data.clear()def test_append(temp_list):temp_list.append(1)assert len(temp_list) 1控制作用域。pytest.fixture的scope决定夹具多久重建一次默认是function每个测试函数一次scope 取值生效范围典型用途function每个测试函数一次默认需要隔离的可变状态class每个测试类一次类级共享资源module每个模块一次模块级只读数据session整个测试会话一次数据库连接、浏览器实例较新版本的 pytest 还提供package作用域用到时请查对应版本文档。# 适用于 Python 3.8import pytestpytest.fixture(scopesession)def config():return {host: localhost, port: 8000}autouseTrue让夹具自动对所有能看见它的测试生效不必写进参数# 适用于 Python 3.8import pytestpytest.fixture(autouseTrue)def reset_counter():state {n: 0}yield statestate[n] 0参数化夹具给夹具传paramspytest 会为每个参数值各跑一遍用它的测试测试里用request.param取值# 适用于 Python 3.8import pytestpytest.fixture(params[1, 2, 3])def number(request):return request.paramdef test_positive(number):assert number 0这条测试会被收集成三个测试用例分别对应1、2、3。夹具写多了以后把它们放进项目根目录或测试目录下的conftest.py同一目录及子目录中的测试都能自动使用不需要import。二、pytest.mark.parametrize参数化这是最常用的标记让同一个测试逻辑跑多组输入。官方签名里第一个参数argnames接受逗号分隔的字符串或字符串列表第二个argvalues是值的列表。# 适用于 Python 3.8import pytestpytest.mark.parametrize(a, b, expected, [(1, 1, 2),(2, 3, 5),(-1, 1, 0),])def test_add(a, b, expected):assert a b expected一次参数化三个参数pytest 会按顺序把元组里的值分配给a、b、expected。想给每组用例起个可读的名字用ids# 适用于 Python 3.8import pytestpytest.mark.parametrize(text, ok, [(ann, True),(1ann, False),], ids[valid, starts-with-digit])def test_name(text, ok):assert text.isidentifier() is okpytest.mark.parametrize可以叠加多次效果是参数组合相乘也可以把参数化标记写在测试类上作用于类里全部方法。三、跳过与预期失败# 适用于 Python 3.8import sysimport pytestpytest.mark.skip(reason功能尚未实现)def test_not_ready():assert Falsepytest.mark.skipif(sys.version_info (3, 10), reason需要 Python 3.10 及以上)def test_new_syntax():assert Truepytest.mark.xfail(reason已知缺陷等待修复)def test_known_bug():assert 1 2三者的差别要说清楚skip无条件跳过直接不跑。skipif(condition, reason...)condition为真时跳过常用来按平台或 Python 版本决定。xfail预期失败。测试照跑失败算「符合预期」标记为 xfailed意外通过会记成 xpass。四、自定义标记与注册pytest.mark.slow这类自定义标记不会自动被识别没注册过会给出警告。正确做法是在pytest.ini里登记[pytest]markers slow: 标记运行较慢的测试integration: 标记集成测试登记之后就能用命令行筛选pytest -m slow # 只跑 slow 标记的测试pytest -m not slow # 跳过 slow 标记也可以把标记加在类或整个模块上模块级需要用pytestmark变量。五、usefixtures只借夹具不带参数有时你只想「让夹具生效」并不需要它返回的东西尤其是autouse之外还需要显式触发的场景就用pytest.mark.usefixtures# 适用于 Python 3.8import pytestpytest.fixturedef prepare_dir(tmp_path):target tmp_path / datatarget.mkdir()return targetpytest.mark.usefixtures(prepare_dir)def test_something(tmp_path):assert (tmp_path / data).is_dir()注意usefixtures不把夹具的返回值传进来测试函数要用返回值还是得写成参数。六、一个完整的实战文件# 适用于 Python 3.8import pytestclass Calculator:def __init__(self, precision2):self.precision precisiondef divide(self, a, b):if b 0:raise ZeroDivisionError(除数不能为零)return round(a / b, self.precision)pytest.fixturedef calc():return Calculator()pytest.mark.parametrize(a, b, expected, [(10, 2, 5.0),(1, 3, 0.33),])def test_divide_ok(calc, a, b, expected):assert calc.divide(a, b) expecteddef test_divide_by_zero(calc):with pytest.raises(ZeroDivisionError):calc.divide(1, 0)pytest.mark.skipif(False, reason示例条件不成立时不跳过)def test_precision():assert Calculator(precision4).divide(1, 3) 0.3333pytest.raises是断言「一定会抛这个异常」的标准写法比try/except手写更简洁也更可靠。常见坑点以为 pytest 装饰器是普通包装装饰器❌ 给pytest.fixture里的函数加functools.wraps想「保留元信息」 ✅ 它们由 pytest 在收集阶段解析按官方文档的用法直接写即可夹具名和参数名不一致❌ 夹具叫db_conn测试参数写成db✅ 参数名必须和夹具名完全相同pytest 就是靠名字注入的夹具里的清理写在return之后❌return data然后指望后面的data.clear()会执行 ✅ 需要清理就用yieldyield data清理代码写在yield之后scope和可变状态一起用❌scopesession的夹具返回可变列表测试之间互相污染 ✅ 需要隔离的状态用默认的function作用域自定义 mark 没注册❌ 直接写pytest.mark.slow跑测试时满屏警告 ✅ 在pytest.ini的[pytest]里加markers登记usefixtures当参数用❌pytest.mark.usefixtures(tmp)然后在函数体里用tmp✅ 要用返回值就必须写成函数参数usefixtures只负责触发parametrize的 argnames 与元组长度不匹配❌ 声明a, b却传(1, 2, 3)收集时报错 ✅ 保证每组值的个数与参数名个数一致用skip掩盖真实失败❌ 测试挂了就加pytest.mark.skip让它变绿 ✅ 已知缺陷用xfail并写清reason修好后能被发现总结装饰器分类作用关键参数pytest.fixture夹具注入准备数据/资源scope、params、autousepytest.mark.parametrize标记同一逻辑跑多组数据argnames、argvalues、idspytest.mark.skip标记无条件跳过reasonpytest.mark.skipif标记条件跳过condition、reasonpytest.mark.xfail标记预期失败reason、raisespytest.mark.usefixtures标记只触发夹具不带值夹具名把握两条就够pytest.fixture管「依赖从哪来」pytest.mark.*管「这个测试怎么跑」。夹具靠参数名注入参数化靠argnames和argvalues对齐跳过与预期失败要分清是「不跑」还是「跑但允许失败」——剩下都是查官方文档能确认的细节。
返回列表