☰
Python pytest装饰器总结(实例详解)
2026/10/8 13:02:25 网站建设 项目流程

前言


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.8+
import pytest


@pytest.fixture
def sample_data():
return [1, 2, 3]


def test_sum(sample_data):
assert sum(sample_data) == 6

用yield做前后置清理——yield之前是准备,之后是清理,即使测试失败清理也会执行:


# 适用于 Python 3.8+
import pytest


@pytest.fixture
def temp_list():
data = []
print("准备")
yield data
print("清理")
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.8+
import pytest


@pytest.fixture(scope="session")
def config():
return {"host": "localhost", "port": 8000}

autouse=True让夹具自动对所有能看见它的测试生效,不必写进参数:


# 适用于 Python 3.8+
import pytest


@pytest.fixture(autouse=True)
def reset_counter():
state = {"n": 0}
yield state
state["n"] = 0

参数化夹具:给夹具传params,pytest 会为每个参数值各跑一遍用它的测试,测试里用request.param取值:


# 适用于 Python 3.8+
import pytest


@pytest.fixture(params=[1, 2, 3])
def number(request):
return request.param


def test_positive(number):
assert number > 0

这条测试会被收集成三个测试用例,分别对应1、2、3。


夹具写多了以后,把它们放进项目根目录或测试目录下的conftest.py,同一目录及子目录中的测试都能自动使用,不需要import。


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


这是最常用的标记,让同一个测试逻辑跑多组输入。官方签名里第一个参数argnames接受逗号分隔的字符串,或字符串列表;第二个argvalues是值的列表。


# 适用于 Python 3.8+
import pytest


@pytest.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.8+
import pytest


@pytest.mark.parametrize("text, ok", [
("ann", True),
("1ann", False),
], ids=["valid", "starts-with-digit"])
def test_name(text, ok):
assert text.isidentifier() is ok

@pytest.mark.parametrize可以叠加多次,效果是参数组合相乘;也可以把参数化标记写在测试类上,作用于类里全部方法。


三、跳过与预期失败


# 适用于 Python 3.8+
import sys
import pytest


@pytest.mark.skip(reason="功能尚未实现")
def test_not_ready():
assert False


@pytest.mark.skipif(sys.version_info < (3, 10), reason="需要 Python 3.10 及以上")
def test_new_syntax():
assert True


@pytest.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.8+
import pytest


@pytest.fixture
def prepare_dir(tmp_path):
target = tmp_path / "data"
target.mkdir()
return target


@pytest.mark.usefixtures("prepare_dir")
def test_something(tmp_path):
assert (tmp_path / "data").is_dir()

注意usefixtures不把夹具的返回值传进来,测试函数要用返回值还是得写成参数。


六、一个完整的实战文件


# 适用于 Python 3.8+
import pytest


class Calculator:
def __init__(self, precision=2):
self.precision = precision

def divide(self, a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零")
return round(a / b, self.precision)


@pytest.fixture
def 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) == expected


def test_divide_by_zero(calc):
with pytest.raises(ZeroDivisionError):
calc.divide(1, 0)


@pytest.mark.skipif(False, reason="示例:条件不成立时不跳过")
def test_precision():
assert Calculator(precision=4).divide(1, 3) == 0.3333

pytest.raises是断言「一定会抛这个异常」的标准写法,比try/except手写更简洁也更可靠。


常见坑点



  1. 以为 pytest 装饰器是普通包装装饰器


❌ 给@pytest.fixture里的函数加functools.wraps想「保留元信息」 ✅ 它们由 pytest 在收集阶段解析,按官方文档的用法直接写即可



  1. 夹具名和参数名不一致


❌ 夹具叫db_conn,测试参数写成db✅ 参数名必须和夹具名完全相同,pytest 就是靠名字注入的



  1. 夹具里的清理写在return之后


❌return data然后指望后面的data.clear()会执行 ✅ 需要清理就用yield:yield data,清理代码写在yield之后



  1. scope和可变状态一起用


❌scope="session"的夹具返回可变列表,测试之间互相污染 ✅ 需要隔离的状态用默认的function作用域



  1. 自定义 mark 没注册


❌ 直接写@pytest.mark.slow,跑测试时满屏警告 ✅ 在pytest.ini的[pytest]里加markers登记



  1. usefixtures当参数用


❌@pytest.mark.usefixtures("tmp")然后在函数体里用tmp✅ 要用返回值就必须写成函数参数;usefixtures只负责触发



  1. parametrize的 argnames 与元组长度不匹配


❌ 声明"a, b"却传(1, 2, 3),收集时报错 ✅ 保证每组值的个数与参数名个数一致



  1. 用skip掩盖真实失败


❌ 测试挂了就加@pytest.mark.skip让它变绿 ✅ 已知缺陷用xfail并写清reason,修好后能被发现


总结




装饰器分类作用关键参数



@pytest.fixture夹具注入准备数据/资源scope、params、autouse

@pytest.mark.parametrize标记同一逻辑跑多组数据argnames、argvalues、ids

@pytest.mark.skip标记无条件跳过reason

@pytest.mark.skipif标记条件跳过condition、reason

@pytest.mark.xfail标记预期失败reason、raises

@pytest.mark.usefixtures标记只触发夹具不带值夹具名



把握两条就够:@pytest.fixture管「依赖从哪来」,@pytest.mark.*管「这个测试怎么跑」。夹具靠参数名注入,参数化靠argnames和argvalues对齐,跳过与预期失败要分清是「不跑」还是「跑但允许失败」——剩下都是查官方文档能确认的细节。





需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询