Pytest:Marker(标记)详解
Pytest:Marker(标记)详解
- 前言
- 一、 什么是 Pytest Marker?
- 二、 Pytest 内置 Marker 详解
- 1. `@pytest.mark.skip` - 无条件跳过
- 2. `@pytest.mark.skipif` - 条件性跳过
- 3. `@pytest.mark.xfail` - 标记预期失败
- 4. `@pytest.mark.parametrize` - 参数化测试
- 5. `@pytest.mark.usefixtures` - 显式使用 Fixture
- 6. `@pytest.mark.filterwarnings` - 控制测试期间的警告
- 三、 自定义 Marker 与注册
- 1. 使用自定义 Marker
- 2. 注册 Marker
- 3. Marker 的继承与覆盖
- 四、 使用 `-m` 表达式进行筛选
- 五、 高级应用:Marker 与 Fixture 及 Hook 的交互
- 1. 在 Fixture 中访问 Marker
- 2. 在 Hook 中使用 Marker
- 六、 Marker 使用的最佳实践
- 总结
前言
- 今天,我想和大家深入探讨 pytest 中一个重要且强大的特性——Marker(标记);
- 对于初学者来说,Marker 可能只是一个简单的标签;但对于经验丰富的工程师而言,Marker 是组织、筛选、控制和扩展测试用例集的瑞士军刀;
- 它能显著提升大型项目中测试套件的管理效率和执行灵活性。
一、 什么是 Pytest Marker?
当你正在维护一个庞大而复杂的项目时,其测试套件可能包含成百上千条测试用例,涵盖了单元测试、集成测试、API 测试、UI 测试、性能测试等多个维度。现在,你面临一些常见的场景:
- 快速反馈:只想运行最核心的“冒烟测试(smoke test)”,确保主要功能没问题。
- 特定环境:某些测试只能在特定操作系统(如 Linux)或特定 Python 版本下运行。
- 功能模块:只想运行与某个特定功能模块(如“用户管理”或“支付模块”)相关的测试。
- 缺陷跟踪:某个测试因为已知缺陷暂时失败,希望跳过它,但明确标记原因,并在缺陷修复后重新启用。
- 预期失败:某个功能正在重构,对应的测试目前预期会失败(Expected Fail, XFAIL),但不希望它被计为“失败(Failed)”。
- 性能考量:某些测试(如性能测试或端到端测试)运行时间较长,希望在常规 CI 流程中排除它们,只在特定阶段运行。
- 特殊配置:某些测试需要特定的环境设置或Fixture组合。
如果没有有效的组织和筛选机制,管理和执行这些测试将变得异常困难和低效。你可能需要维护多个不同的测试文件列表,或者在代码中嵌入复杂的逻辑判断,这都会增加维护成本和出错的风险。
这就是 Pytest Marker 发挥作用的地方!
Pytest Marker 本质上是应用于测试函数、类或整个模块的元数据(metadata)标签。 它们就像给你的测试用例贴上了各种分类清晰、功能明确的“标签”。通过这些标签,pytest 及其插件可以识别、筛选和处理测试用例,实现精细化的控制。
使用 Marker 的核心优势在于:
- 分类与组织:将测试按功能、类型、优先级等维度进行逻辑分组。
- 选择性执行:通过命令行选项
-m
灵活地选择要运行或排除的测试子集。 - 条件化执行:根据特定条件(如环境、依赖、配置)决定是否跳过(skip)或标记为预期失败(xfail)。
- 行为定制:结合 pytest 钩子(hooks)和插件,可以基于 marker 实现自定义的测试行为。
- 增强可读性:Marker 本身可以作为一种文档,说明测试的特性或意图。
可以说,熟练运用 Marker 是衡量一个测试工程师是否能高效利用 pytest 的重要标准之一。
二、 Pytest 内置 Marker 详解
Pytest 提供了一系列非常有用的内置 Marker,覆盖了许多常见的测试场景。让我们逐一深入了解它们。
1. @pytest.mark.skip
- 无条件跳过
这是最直接的 Marker 之一,用于标记某个测试函数或类应始终被跳过。
使用场景:
- 测试的功能尚未实现。
- 测试本身存在问题,需要修复。
- 测试依赖的外部服务暂时不可用。
- 临时禁用某个不稳定的测试。
示例:
import pytest
import sys
# 简单跳过
@pytest.mark.skip(reason="功能尚未实现")
def test_new_feature():
assert False # 这部分代码不会被执行
# 在类上应用,跳过整个类中的所有测试
@pytest.mark.skip(reason="整个模块正在重构")
class TestPaymentModule:
def test_alipay_integration(self):
pass
def test_wechatpay_integration(self):
pass
# 可以在模块级别应用 (使用 pytestmark 变量)
# pytestmark = pytest.mark.skip("模块维护中")
# def test_module_level_func():
# pass
执行结果:
当运行 pytest
时,被 @pytest.mark.skip
标记的测试会被报告为 SKIPPED
(或 s
),并显示跳过的原因(如果提供了 reason
参数)。
关键点:
reason
参数是可选的,但强烈建议提供,以便清晰地说明跳过的原因,方便后续跟踪。skip
是无条件的,一旦标记,该测试总会被跳过。
2. @pytest.mark.skipif
- 条件性跳过
skipif
比 skip
更智能,它允许你基于一个布尔表达式来决定是否跳过测试。只有当条件为 True
时,测试才会被跳过。
使用场景:
- 测试仅适用于特定操作系统。
- 测试依赖于某个特定版本的 Python 或库。
- 测试需要某个环境变量或配置文件存在。
- 测试依赖于某个外部服务是否可达。
示例:
import pytest
import sys
import os
# 基于 Python 版本跳过
needs_python310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="需要 Python 3.10+")
@needs_python310
def test_using_match_case():
# 假设这里用了 3.10 的 match-case 语法
value = 42
match value:
case 42:
assert True
case _:
assert False
# 基于操作系统跳过
@pytest.mark.skipif(sys.platform != "win32", reason="仅在 Windows 上运行")
def test_windows_specific_api():
# 调用 Windows API 的代码
pass
# 基于环境变量跳过
api_key_present = pytest.mark.skipif("MY_API_KEY" not in os.environ, reason="需要设置 MY_API_KEY 环境变量")
@api_key_present
def test_api_with_key():
api_key = os.environ["MY_API_KEY"]
# 使用 API Key 进行测试
pass
# 使用自定义函数进行判断 (更复杂的逻辑)
def is_database_connected():
# 假设这里有检查数据库连接的逻辑
try:
# connect_to_db()
return False # 模拟数据库未连接
except Exception:
return True
@pytest.mark.skipif(is_database_connected() == False, reason="数据库未连接")
def test_database_operations():
# 执行数据库操作
pass
# 可以在类上应用
@pytest.mark.skipif(sys.platform == 'darwin', reason="MacOS 上暂时跳过")
class TestFeatureX:
def test_sub_feature_a(self):
pass
def test_sub_feature_b(self):
pass
执行结果:
Pytest 会在收集测试用例时评估 skipif
的条件表达式。如果条件为 True
,测试被标记为 SKIPPED
并显示原因;如果为 False
,测试正常执行。
关键点:
skipif
的第一个参数是一个布尔表达式(字符串形式或者直接是布尔值)。Pytest 会在测试函数的上下文中评估这个表达式。reason
参数同样强烈推荐使用。skipif
使得测试套件更具适应性和鲁棒性,能够根据环境自动调整执行范围。
3. @pytest.mark.xfail
- 标记预期失败
xfail
(Expected Failure) 用于标记那些你预期会失败的测试。这与 skip
不同,xfail
标记的测试 仍然会被执行。
使用场景:
- 代码中存在一个已知但尚未修复的 Bug,导致某个测试失败。
- 某个功能正在开发或重构中,其测试暂时无法通过,但你希望跟踪它。
- 测试依赖的外部系统存在问题,导致测试必然失败。
示例:
import pytest
import sys
@pytest.mark.xfail(reason="已知 Bug #123 尚未修复")
def test_known_bug():
assert 1 == 2 # 这个断言会失败
@pytest.mark.xfail(sys.version_info >= (3, 11), reason="功能在 Python 3.11+ 上行为异常")
def test_compat_issue():
if sys.version_info >= (3, 11):
assert False # 在 3.11+ 上预期失败
else:
assert True # 在旧版本上预期通过
# 控制严格性 (strict=True)
@pytest.mark.xfail(strict=True, reason="必须失败,否则说明 Bug 已修复或测试过时")
def test_strict_xfail():
# 如果这个测试意外通过了,整个测试运行会失败 (F)
assert calculate_something_buggy() == expected_wrong_result
# 预期特定异常
@pytest.mark.xfail(raises=ValueError, reason="预期会抛出 ValueError")
def test_expecting_value_error():
raise ValueError("这是预期的错误")
def calculate_something_buggy():
# 模拟一个有 bug 的函数
return 0
expected_wrong_result = 0
执行结果:
- 如果标记为
xfail
的测试执行后 失败 了(如预期),pytest 会报告为XFAIL
(或x
)。这 不会 导致整个测试套件失败。 - 如果标记为
xfail
的测试执行后 意外地通过 了,pytest 会报告为XPASS
(或X
)。默认情况下,这 不会 导致整个测试套件失败。 strict=True
参数:如果设置了strict=True
,那么XPASS
(意外通过) 将被视为 测试套件的失败 (Failed)。这很有用,它可以提醒你:要么是底层的 Bug 已经被修复了(你需要移除xfail
),要么是你的测试本身可能写得不对或过时了。raises=ExceptionType
参数:你可以指定一个预期的异常类型。只有当测试失败并且抛出了指定的异常(或其子类)时,才算作XFAIL
。如果抛出了不同的异常或没有抛出异常,则算作普通的FAILED
(F)。
关键点:
xfail
用于管理那些你知道会失败但暂时无法修复或不需要立即修复的情况。- 它有助于区分“真正的问题”(Failed)和“已知的问题”(XFAIL)。
strict=True
是一个非常有用的实践,可以防止xfail
标记被遗忘或滥用。
4. @pytest.mark.parametrize
- 参数化测试
严格来说,parametrize
主要功能是实现参数化测试(Data-Driven Testing),而不是像 skip
或 xfail
那样用于标记和筛选。但它使用了 @pytest.mark
的语法,是 Marker 体系中极其重要的一环。它允许你使用不同的参数值多次运行同一个测试函数。
使用场景:
- 测试同一个函数对不同输入值的响应。
- 测试边界条件和异常情况。
- 减少重复的测试代码。
示例:
import pytest
# 单个参数
@pytest.mark.parametrize("input_val, expected_output", [
(1, 2),
(2, 3),
(0, 1),
(-1, 0),
pytest.param(10, 11, marks=pytest.mark.smoke), # 给特定参数组打标记
pytest.param(100, 101, marks=pytest.mark.slow, id="large_number_case") # 自定义 ID 和标记
])
def test_increment(input_val, expected_output):
assert input_val + 1 == expected_output
# 多个参数组合
@pytest.mark.parametrize("a", [1, 2])
@pytest.mark.parametrize("b", [10, 20])
def test_combinations(a, b):
# 会生成 2x2 = 4 个测试实例
# (1, 10), (1, 20), (2, 10), (2, 20)
assert isinstance(a, int)
assert isinstance(b, int)
# 使用 pytest.param 定制 ID 和 Marks
users = [
pytest.param("admin", "password123", id="admin_user", marks=pytest.mark.admin),
pytest.param("guest", "", id="guest_user", marks=[pytest.mark.guest, pytest.mark.xfail(reason="Guest login TBD")])
]
@pytest.mark.parametrize("username, password", users)
def test_login(username, password):
# 模拟登录逻辑
print(f"Testing login for {username}")
if username == "guest":
assert False # 预期失败
# ... 其他断言 ...
执行结果:
parametrize
会根据提供的参数列表,为每个参数组合生成一个独立的测试实例。在报告中,你会看到多次执行同一个测试函数,但带有不同的参数标识。
关键点:
- 第一个参数是包含参数名的字符串(逗号分隔)或列表/元组。
- 第二个参数是包含参数值的列表或元组。列表中的每个元素通常是一个元组(对应参数名)或单个值(如果只有一个参数)。
- 可以使用
pytest.param(value1, value2, ..., id="custom_id", marks=marker_or_list)
来为特定的参数组合设置自定义的测试 ID(用于报告和选择)和应用额外的 Marker(如skip
,xfail
, 或自定义 Marker)。 - 多个
parametrize
装饰器会产生笛卡尔积的参数组合。 - 虽然
parametrize
本身不是筛选标记,但你可以通过pytest.param
给特定的参数化实例打上筛选标记(如smoke
,slow
等),然后使用-m
来选择运行它们。
5. @pytest.mark.usefixtures
- 显式使用 Fixture
通常,pytest 通过将 fixture 名称作为测试函数的参数来自动应用 fixture。但有时,你可能需要一个 fixture 的副作用(比如设置环境、启动服务),但不需要在测试函数中直接使用它的返回值。这时可以使用 @pytest.mark.usefixtures
。
使用场景:
- 需要运行 setup/teardown 代码,但测试逻辑不直接与 fixture 返回值交互。
- 希望在类或模块级别应用某个 setup/teardown fixture,影响多个测试。
示例:
import pytest
@pytest.fixture(scope="module")
def setup_database():
print("\nSetting up database...")
# 实际的数据库设置代码
yield
print("\nTearing down database...")
# 实际的数据库清理代码
@pytest.mark.usefixtures("setup_database")
class TestDatabaseFeatures:
def test_read_data(self):
# 虽然 setup_database 运行了,但这里不需要它的返回值
print("Running test_read_data")
assert True
def test_write_data(self):
print("Running test_write_data")
assert True
@pytest.fixture()
def clean_temp_files():
print("\nCleaning temp files before test...")
# 清理逻辑
yield
print("\nCleaning temp files after test...")
# 清理逻辑
@pytest.mark.usefixtures("clean_temp_files")
def test_file_processing():
print("Running test_file_processing")
# 执行文件处理操作,不需要 fixture 返回值
assert True
执行结果:
被 usefixtures
指定的 fixture 会在测试执行前被调用(执行 setup 部分),并在测试执行后被调用(执行 teardown 部分),即使测试函数签名中没有这个 fixture 参数。
关键点:
- 参数是 fixture 的名称(字符串),可以是一个或多个(以逗号分隔的字符串,或列表/元组)。
usefixtures
可以应用于函数、类或模块(通过pytestmark
)。- 当 fixture 仅用于 setup/teardown 而测试逻辑不依赖其返回值时,使用
usefixtures
可以让测试函数签名更简洁。
6. @pytest.mark.filterwarnings
- 控制测试期间的警告
有时,你的代码或其依赖库可能会产生一些你知道是良性或暂时无法解决的警告(Warnings)。这些警告会干扰 pytest 的输出。filterwarnings
Marker 允许你精细地控制在特定测试执行期间如何处理这些警告。
使用场景:
- 忽略特定类型的弃用警告(DeprecationWarning)。
- 将某个特定的警告视为错误。
- 仅关注某些模块产生的警告。
示例:
import pytest
import warnings
def function_that_warns():
warnings.warn("This is a custom warning", UserWarning)
return True
# 忽略所有 UserWarning
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_ignore_user_warning():
assert function_that_warns()
# 忽略特定消息的警告
@pytest.mark.filterwarnings("ignore:Specific warning message")
def test_ignore_specific_message():
warnings.warn("Specific warning message", DeprecationWarning)
assert True
# 将特定警告视为错误
@pytest.mark.filterwarnings("error::RuntimeWarning")
def test_error_on_runtime_warning():
with pytest.raises(RuntimeWarning): # 测试会失败,因为 RuntimeError 被视为错误抛出
warnings.warn("This should cause an error", RuntimeWarning)
# 可以在类或模块级别应用
# @pytest.mark.filterwarnings("ignore") # 忽略所有警告
# class TestQuietly:
# ...
执行结果:
Pytest 会根据 filterwarnings
的规则来处理测试执行期间产生的警告,可能包括忽略、报告、或将其升级为错误。
关键点:
- 其语法遵循 Python
warnings
模块的过滤器字符串格式:action:message:category:module:lineno
。 - 常用的
action
包括ignore
,error
,always
,default
,module
,once
。 - 这是一个控制测试环境“噪音”的有用工具。
三、 自定义 Marker 与注册
除了内置 Marker,pytest 真正强大的地方在于允许你创建 自定义 Marker。这使得你可以根据项目的具体需求,对测试进行任意维度的标记和分类。
使用场景:
- 按功能模块标记:
@pytest.mark.user_auth
,@pytest.mark.payment
,@pytest.mark.search
- 按测试类型标记:
@pytest.mark.api
,@pytest.mark.ui
,@pytest.mark.performance
- 按优先级标记:
@pytest.mark.p0
,@pytest.mark.p1
- 按测试环境标记:
@pytest.mark.staging_only
,@pytest.mark.production_safe
- 按特性标记:
@pytest.mark.regression
,@pytest.mark.smoke
,@pytest.mark.sanity
1. 使用自定义 Marker
使用自定义 Marker 非常简单,就像使用内置 Marker 一样,直接用 @pytest.mark.your_marker_name
装饰测试函数、类或模块。
import pytest
@pytest.mark.smoke
@pytest.mark.api
def test_login_api_success():
# ... 测试登录 API 成功场景 ...
pass
@pytest.mark.ui
@pytest.mark.regression
class TestUserProfileUI:
def test_update_avatar(self):
# ... 测试更新头像的 UI 流程 ...
pass
def test_change_password(self):
# ... 测试修改密码的 UI 流程 ...
pass
# 模块级别的标记
pytestmark = [pytest.mark.payment, pytest.mark.p1]
def test_create_order():
# ... 测试创建订单功能 ...
pass
执行结果:
2. 注册 Marker
虽然可以直接使用未注册的自定义 Marker,但 pytest 会发出警告(PytestUnknownMarkWarning
),并且在使用 -m
筛选时,如果 Marker 名称拼写错误,pytest 也不会报错,这可能导致意外的行为。
强烈建议注册你所有的自定义 Marker! 注册的好处:
- 消除警告:让你的测试输出更干净。
- 拼写检查:如果使用了未注册的 Marker,pytest 会明确提示。
- 文档说明:可以在注册时为 Marker 添加描述,方便团队成员理解其含义。
- 支持带参数的 Marker:某些高级用法需要注册。
注册方法:
最常用的方法是在项目根目录下的配置文件中注册。可以是以下任一文件:
pytest.ini
pyproject.toml
(推荐,现代 Python 项目标准)tox.ini
在 pytest.ini
中注册:
[pytest]
markers =
smoke: Marks tests as smoke tests (fast, basic functionality check).
regression: Marks tests as regression tests.
api: Marks API tests.
ui: Marks UI tests.
p0: High priority tests.
p1: Medium priority tests.
slow: Marks tests as slow running.
database(env): Marks tests that require a specific database environment.
在 pyproject.toml
中注册:
[tool.pytest.ini_options]
markers = [
"smoke: Marks tests as smoke tests (fast, basic functionality check).",
"regression: Marks tests as regression tests.",
"api: Marks API tests.",
"ui: Marks UI tests.",
"p0: High priority tests.",
"p1: Medium priority tests.",
"slow: Marks tests as slow running.",
"database(env): Marks tests that require a specific database environment.",
]
解释:
- 每一行定义一个 Marker。
- 格式为
marker_name: description
。描述是可选的,但强烈推荐。 - 如果 Marker 可以接受参数(例如上面例子中的
database(env)
),在注册时可以用括号()
表示,但这主要是文档性的,并不强制参数的存在或类型。
注册后,运行 pytest 时就不会再看到未知 Marker 的警告了。
3. Marker 的继承与覆盖
Marker 的应用具有继承性:
- 模块级别的 Marker (
pytestmark
) 应用于该模块下的所有类和函数。 - 类级别的 Marker 应用于该类下的所有方法。
- 函数级别的 Marker 只应用于该函数。
如果同一层级(如函数上)有多个 Marker,它们会累加。
如果不同层级有相同的 Marker,通常会以最具体的层级为准,但对于筛选来说,只要任何层级有该 Marker,就会被选中。例如:
import pytest
pytestmark = pytest.mark.module_level # 应用于模块所有测试
@pytest.mark.class_level
class TestMarkers:
@pytest.mark.function_level
def test_example(self):
pass # 这个测试会同时拥有 module_level, class_level, function_level 三个标记
def test_another(self):
pass # 这个测试拥有 module_level, class_level 两个标记
对于 skip
, skipif
, xfail
这类控制执行的 Marker,如果在多个层级应用,它们的行为可能会叠加或覆盖,具体取决于 Marker 的类型和 pytest 的处理逻辑。通常,最内层的(如函数级)skip
或 skipif
会优先于外层的。
四、 使用 -m
表达式进行筛选
Marker 最强大的应用场景之一就是通过命令行的 -m
选项来选择性地执行测试。-m
选项支持复杂的逻辑表达式。
基本用法:
pytest -m smoke
:只运行标记为smoke
的测试。pytest -m "not smoke"
:运行所有 未 标记为smoke
的测试。pytest -m "api and regression"
:只运行 同时 标记为api
和regression
的测试。pytest -m "ui or api"
:运行标记为ui
或api
的测试。pytest -m "(p0 or p1) and not slow"
:运行优先级为 P0 或 P1,但 不是 标记为slow
的测试。pytest -m "database"
:运行所有标记为database
的测试,无论是否有参数。pytest -m "database(production)"
:通常用于结合插件或钩子,筛选标记为database
且参数为production
的情况(注意:原生-m
不直接支持按参数值筛选,但这可以通过自定义钩子实现)。
关键点:
- 表达式需要用引号括起来,特别是包含空格或特殊字符(如
and
,or
,not
,(
,)
) 时。 not
优先级最高,然后是and
,最后是or
。可以使用括号改变优先级。- Marker 名称区分大小写(除非 pytest 配置中修改了)。
- 这是在 CI/CD 流程中实现不同测试阶段(如冒烟测试、全量回归测试、特定模块测试)的关键机制。
五、 高级应用:Marker 与 Fixture 及 Hook 的交互
Marker 的威力远不止于分类和筛选。它们可以与 pytest 的 Fixture 和 Hook 系统深度集成,实现更复杂的测试逻辑和流程控制。
1. 在 Fixture 中访问 Marker
Fixture 可以通过 request
对象检查应用到测试项(函数/类/模块)上的 Marker,并据此调整自己的行为。
import pytest
# 假设在 pytest.ini 或 pyproject.toml 中注册了 marker:
# db_type(type): Specifies the required database type (e.g., postgres, mysql)
@pytest.fixture(scope="function")
def db_connection(request):
marker = request.node.get_closest_marker("db_type")
if marker is None:
# 默认连接或抛出错误
db_type = "default_db"
print(f"No db_type marker found, using {db_type}")
else:
db_type = marker.args[0] # 获取 marker 的第一个参数
print(f"db_type marker found, requested type: {db_type}")
# 根据 db_type 建立不同的数据库连接
if db_type == "postgres":
conn = connect_to_postgres()
elif db_type == "mysql":
conn = connect_to_mysql()
else:
conn = connect_to_default()
yield conn # 返回连接对象
print(f"Closing connection for {db_type}")
conn.close()
# --- 模拟连接函数 ---
class MockConnection:
def __init__(self, db_type): self.db_type = db_type
def close(self): print(f"Closing mock {self.db_type} connection")
def connect_to_postgres(): return MockConnection("postgres")
def connect_to_mysql(): return MockConnection("mysql")
def connect_to_default(): return MockConnection("default")
# --- End Mock ---
@pytest.mark.db_type("postgres")
def test_with_postgres(db_connection):
assert db_connection.db_type == "postgres"
print("Running test requiring postgres")
@pytest.mark.db_type("mysql")
def test_with_mysql(db_connection):
assert db_connection.db_type == "mysql"
print("Running test requiring mysql")
def test_with_default_db(db_connection):
assert db_connection.db_type == "default_db"
print("Running test requiring default db")
在这个例子中,db_connection
fixture 会检查调用它的测试函数上是否有 db_type
Marker。如果有,它会读取 Marker 的参数(数据库类型),并建立相应的数据库连接。这使得 Fixture 更加智能和可配置。
关键点:
request.node
代表当前的测试项(函数、类或模块节点)。request.node.get_closest_marker("marker_name")
返回最接近的指定名称的 Marker 对象(查找顺序:函数 -> 类 -> 模块)。如果找不到则返回None
。- Marker 对象有
name
(字符串),args
(元组),kwargs
(字典) 属性,可以访问传递给 Marker 的参数。
2. 在 Hook 中使用 Marker
Pytest 的 Hook 允许你在测试的不同阶段插入自定义逻辑。Marker 是 Hook 函数中识别和操作特定测试项的重要依据。
一个常见的例子是使用 pytest_collection_modifyitems
Hook 来动态地修改或筛选测试项。
# conftest.py (或者插件文件)
import pytest
# 假设注册了 marker: slow, external_service
def pytest_collection_modifyitems(config, items):
"""
在收集完所有测试项后进行修改。
"""
skip_slow = config.getoption("--skip-slow")
skip_external = config.getoption("--skip-external")
keyword = config.getoption("-k") # 获取 -k 关键字筛选
marker_expr = config.getoption("-m") # 获取 -m 标记筛选
# 如果没有命令行选项,也可以在这里硬编码逻辑
# For example, always skip 'slow' tests in a specific CI environment
for item in items:
# item 是一个 Test Item 对象 (通常是 Function)
# 场景1: 根据命令行选项跳过测试
if skip_slow and item.get_closest_marker("slow"):
item.add_marker(pytest.mark.skip(reason="Skipped due to --skip-slow flag"))
if skip_external and item.get_closest_marker("external_service"):
item.add_marker(pytest.mark.skip(reason="Skipped due to --skip-external flag"))
# 场景2: 动态添加标记 (例如,给所有 UI 测试添加一个通用标记)
# if "ui" in item.keywords: # 'keywords' 包含文件名、类名、函数名和 markers
# item.add_marker(pytest.mark.needs_browser)
# 注意:pytest 自身已经处理了 -m 筛选,这里通常是做更复杂的、
# 基于 marker 的 *补充* 逻辑,或者动态 *添加* marker。
def pytest_configure(config):
"""
注册自定义命令行选项。
"""
config.addinivalue_line(
"markers", "slow: marks tests as slow running (can be skipped with --skip-slow)"
)
config.addinivalue_line(
"markers", "external_service: marks tests that depend on external services (can be skipped with --skip-external)"
)
config.addinivalue_line(
"markers", "needs_browser: dynamically added marker for UI tests"
)
config.option.skip_slow = None # 添加选项占位符
config.option.skip_external = None
def pytest_addoption(parser):
"""
添加命令行选项。
"""
parser.addoption(
"--skip-slow", action="store_true", default=False, help="Skip tests marked as slow"
)
parser.addoption(
"--skip-external", action="store_true", default=False, help="Skip tests marked as external_service"
)
在这个 conftest.py
示例中:
- 我们定义了两个自定义命令行选项
--skip-slow
和--skip-external
。 - 在
pytest_collection_modifyitems
Hook 中,我们遍历所有收集到的测试项 (items
)。 - 对于每个测试项,我们检查它是否有
slow
或external_service
Marker。 - 如果对应的命令行选项被激活,我们就动态地给这个测试项 添加 一个
pytest.mark.skip
Marker,从而跳过它。 - 我们也注册了这些 Marker 以避免警告。
这展示了如何结合 Marker 和 Hook 来实现非常灵活的测试执行控制逻辑,超越了简单的 -m
筛选。
其他可以用到 Marker 的 Hook 示例:
pytest_runtest_setup(item)
:在每个测试项执行 setup 之前调用。可以根据item
上的 Marker 执行特定的准备工作。pytest_runtest_logreport(report)
:在测试报告生成时调用。可以根据report.keywords
(包含 Marker)自定义报告内容或执行特定操作(如集成到测试管理系统)。
六、 Marker 使用的最佳实践
要充分发挥 Marker 的优势并避免混乱,建议遵循一些最佳实践:
- 始终注册你的 Marker:在
pytest.ini
或pyproject.toml
中注册所有自定义 Marker,并提供清晰的描述。 - 保持 Marker 语义清晰:Marker 名称应简洁明了,准确反映其含义。避免使用过于模糊或通用的名称。
- 建立 Marker 规范:在团队内部(或项目文档中)明确定义常用 Marker 的含义和使用场景,保持一致性。
- 不要过度使用 Marker:并非每个测试都需要标记。只在确实需要分类、筛选或特殊处理时才添加 Marker。过多的 Marker 会增加维护负担。
- 优先使用内置 Marker:对于
skip
,skipif
,xfail
等常见场景,优先使用 pytest 提供的内置 Marker,它们的行为是标准化的。 - 合理利用
-m
表达式:掌握and
,or
,not
和括号,编写精确的筛选表达式。 - 结合 Fixture 和 Hook 实现高级控制:对于超出简单筛选的需求,考虑在 Fixture 或 Hook 中读取和利用 Marker 信息。
- 定期审查 Marker:随着项目演进,某些 Marker 可能变得过时或不再需要。定期回顾和清理 Marker 列表。
- 将 Marker 集成到 CI/CD:利用
-m
在不同的 CI/CD 阶段运行不同的测试子集(如:推送时跑smoke
,合并请求时跑regression and not slow
,夜间构建跑all
或slow
)。 - 注意 Marker 参数:如果 Marker 需要接受参数(如
@pytest.mark.config(env='staging')
),确保在注册时(文档性)和在 Fixture/Hook 中使用时正确处理marker.args
或marker.kwargs
。
总结
Pytest Marker 远不止是简单的标签。它们是 pytest 框架中用于 组织、筛选、控制和扩展测试套件的核心机制。从基础的 skip
、xfail
到强大的 parametrize
,再到灵活的自定义 Marker,以及与 Fixture 和 Hook 的深度集成,Marker 为我们管理复杂的测试代码库提供了灵活性。
觉得文章有帮助?欢迎点赞、评论、分享!