mark 标记是 pytest 的特点之一,功能丰富且灵活。官方不仅内置了一些标记,还允许用户创建自定义标记,使用自定义标记来分类测试并运行指定分类测试用例。
跳过标记
pytest 框架提供用于跳过测试用例的标记,它允许跳过测试用例的执行,并在测试报告中显示跳过原因。
无条件跳过测试用例
对于暂时不会执行的测试用例,可以使用 @pytest.mark.skip 标记装饰.
跳过测试函数:
import pytest
@pytest.mark.skip(reason="功能尚未实现")
def test_example():
assert False # 该测试不会执行
运行结果:
========================================================= test session starts =========================================================
collected 1 item
cases/test_a.py::test_example SKIPPED (功能尚未实现) [100%]
========================================================= 1 skipped in 0.01s ==========================================================
跳过测试类:
import pytest
@pytest.mark.skip(reason="整个测试组暂不执行")
class TestSkipClass:
def test_case1(self):
assert True
def test_case2(self):
assert True
运行结果:
======================================================== test session starts ========================================================
collected 2 items
cases/test_a.py::TestSkipClass::test_case1 SKIPPED (整个测试组暂不执行) [ 50%]
cases/test_a.py::TestSkipClass::test_case2 SKIPPED (整个测试组暂不执行) [100%]
======================================================== 2 skipped in 0.01s =========================================================
跳过测试类中的测试函数:
import pytest
class TestSkipClass:
@pytest.mark.skip(reason="功能尚未实现")
def test_case1(self):
assert True
def test_case2(self):
assert True
运行结果:
======================================================== test session starts ========================================================
collected 2 items
cases/test_a.py::TestSkipClass::test_case1 SKIPPED (功能尚未实现) [ 50%]
cases/test_a.py::TestSkipClass::test_case2 PASSED [100%]
=================================================== 1 passed, 1 skipped in 0.01s ====================================================
跳过测试文件:
import pytest
pytestmark = pytest.mark.skip(reason="模块暂不执行")
def test_case1():
assert True
def test_case2():
assert True
运行结果:
======================================================== test session starts ========================================================
collected 2 items
cases/test_a.py::test_case1 SKIPPED (模块暂不执行) [ 50%]
cases/test_a.py::test_case2 SKIPPED (模块暂不执行) [100%]
======================================================== 2 skipped in 0.01s =========================================================
在 pytest 中,所有 mark 标记器的作用域都如 @pytest.mark.skip 一致。
跳过满足特定条件的测试用例
除了无条件跳过测试用例,还可以结合条件判断,跳过满足特定条件的测试用例。
使用 @pytest.mark.skipif 标记装饰器:
import pytest
import sys
# 定义通用标记:在 Windows 平台跳过
@pytest.mark.skipif(
sys.platform == "win32",
reason="不支持 Windows"
)
def test_case():
assert True
自定义条件函数 + pytest.mark.skip
相较于 pytest.mark.skipif,自定义条件函数无疑更灵活,应用场景更多。
- 跳过单独测试用例
import pytest
# 定义条件函数
def condition():
return True
def test_dynamic_skip():
if condition(): # 根据条件动态跳过
pytest.skip("不满足条件,跳过此测试")
assert True
运行结果:
======================================================== test session starts ========================================================
collected 1 item
cases/test_a.py::test_dynamic_skip SKIPPED (不满足条件,跳过此测试) [100%]
======================================================== 1 skipped in 0.01s =========================================================
- 跳过测试组
import pytest
# 定义条件函数
def condition():
return True # 根据实际需求返回 True 或 False
class TestDynamicSkip:
@classmethod
def setup_class(cls):
# 如果条件满足,则跳过整个类
if condition():
pytest.skip("不满足条件,跳过此测试组", allow_module_level=True)
def test_case1(self):
assert True
def test_case2(self):
assert True
运行结果:
======================================================== test session starts ========================================================
collected 2 items
cases/test_a.py::TestDynamicSkip::test_case1 SKIPPED (不满足条件,跳过此测试组) [ 50%]
cases/test_a.py::TestDynamicSkip::test_case2 SKIPPED (不满足条件,跳过此测试组) [100%]
======================================================== 2 skipped in 0.01s =========================================================
- 跳过模块文件
import pytest
# 定义条件函数
def condition():
return True # 根据实际需求返回 True 或 False
if condition():
pytestmark = pytest.mark.skip(reason="不满足条件,模块暂不执行")
def test_case1():
assert True
def test_case2():
assert False
运行结果:
======================================================== test session starts ========================================================
collected 2 items
cases/test_a.py::test_case1 SKIPPED (不满足条件,模块暂不执行) [ 50%]
cases/test_a.py::test_case2 SKIPPED (不满足条件,模块暂不执行) [100%]
======================================================== 2 skipped in 0.01s =========================================================
- 跳过测试套件
用例目录里的 init.py 文件里定义:
import pytest
# 定义条件函数
def condition():
return True # 根据实际需求返回 True 或 False
if condition():
pytestmark = pytest.skip(reason="不满足条件,模块暂不执行", allow_module_level=True)
满足条件后,会跳过这个目录下的所有测试用例。
官方标记预期失败
如果已经知道测试的功能存在问题,可以使用 @pytest.mark.xfail 标记测试用例为预期失败,当断言失败时,不会计入失败;如果断言意外通过成功,可以设置 strict=True 来将其标记为失败。
import pytest
@pytest.mark.xfail(reason="这个测试用例应该不通过", strict=True)
def test_case1():
assert False
@pytest.mark.xfail(reason="这个测试用例应该不通过")
def test_case2():
assert False
@pytest.mark.xfail(reason="这个测试用例应该不通过", strict=True)
def test_case3():
assert True
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0 -- /home/ether/projects/video-service-private-network-gateway/.venv/bin/python3
configfile: pytest.toml
collected 3 items
cases/test_a.py::test_case1 XFAIL (这个测试用例应该不通过) [ 33%]
cases/test_a.py::test_case2 XFAIL (这个测试用例应该不通过) [ 66%]
cases/test_a.py::test_case3 FAILED [100%]
============================================================= FAILURES ==============================================================
____________________________________________________________ test_case3 _____________________________________________________________
[XPASS(strict)] 这个测试用例应该不通过
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_case3 - [XPASS(strict)] 这个测试用例应该不通过
=================================================== 1 failed, 2 xfailed in 0.02s ====================================================
超时标记器
第三方插件 @pytest.mark.timeout 可以捕捉过长的测试时间,在测试用例耗时太长时终止它并标记为 FAILED。我们可以用它测试用例的执行时间以及防止长时间被挂起。
除此之外,还可以查阅pytest插件列表,有很多有用的插件。
安装:
pip install pytest-timeout
在测试用例中添加 @pytest.mark.timeout 装饰器,指定超时时间:
import pytest
import time
@pytest.mark.timeout(5) # 单位为秒
def test_timeout():
time.sleep(10)
def test_bbbb():
assert True
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
plugins: timeout-2.4.0
collected 2 items
cases/test_a.py F. [100%]
============================================================= FAILURES ==============================================================
___________________________________________________________ test_timeout ____________________________________________________________
@pytest.mark.timeout(5) # 单位为秒
def test_timeout():
> time.sleep(10)
E Failed: Timeout (>5.0s) from pytest-timeout.
cases/test_a.py:6: Failed
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_timeout - Failed: Timeout (>5.0s) from pytest-timeout.
==================================================== 1 failed, 1 passed in 5.04s ====================================================
运行时设置超时时间:
import time
def test_timeout():
time.sleep(10)
def test_bbbb():
assert True
运行:
python run.py --timeout=5
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
plugins: timeout-2.4.0
timeout: 5.0s
timeout method: signal
timeout func_only: False
collected 2 items
cases/test_a.py F. [100%]
============================================================= FAILURES ==============================================================
___________________________________________________________ test_timeout ____________________________________________________________
def test_timeout():
> time.sleep(10)
E Failed: Timeout (>5.0s) from pytest-timeout.
cases/test_a.py:4: Failed
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_timeout - Failed: Timeout (>5.0s) from pytest-timeout.
==================================================== 1 failed, 1 passed in 5.05s ====================================================
pytest 配置文件中使用timeout选项设置全局超时:
配置文件:
[pytest]
timeout = "3"
测试用例:
import time
def test_timeout():
time.sleep(10)
def test_bbbb():
assert True
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
plugins: timeout-2.4.0
timeout: 3.0s
timeout method: signal
timeout func_only: False
collected 2 items
cases/test_a.py F. [100%]
============================================================= FAILURES ==============================================================
___________________________________________________________ test_timeout ____________________________________________________________
def test_timeout():
> time.sleep(10)
E Failed: Timeout (>3.0s) from pytest-timeout.
cases/test_a.py:4: Failed
====================================================== short test summary info ======================================================
FAILED cases/test_a.py::test_timeout - Failed: Timeout (>3.0s) from pytest-timeout.
==================================================== 1 failed, 1 passed in 3.04s ====================================================
针对 Windows 平台 ,自定义超时装饰器:
在 Windows 平台上,该标记装饰器有问题,任意一个测试用例超时,会中断所有测试。所以该标记装饰器在 Windows 平台上是不可用的,但是我们可以自定义一个超时装饰器。
下面是我自定义的一个非常简单的超时装饰器,可以编辑在 common/timeout.py 中。
# common/timeout.py
import concurrent.futures
import functools
from typing import Any, Callable
def timeout(seconds: int = 10) -> Callable:
"""
超时装饰器:为函数添加超时限制,若执行时间超过指定秒数则抛出 AssertionError。
:param seconds: 超时时间(单位:秒),默认为 10。
:return: 被包装的函数。
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(func, *args, **kwargs)
try:
return future.result(timeout=seconds)
except concurrent.futures.TimeoutError:
error_msg = f"测试用例 {func.__name__} 已执行超过 {seconds} 秒。"
raise AssertionError(error_msg) from None
return wrapper
return decorator
使用方法如下:
from common.timeout import timeout
import time
@timeout(3)
def test_case1():
time.sleep(5)
assert True
@timeout(5)
def test_case2():
time.sleep(3)
assert True
运行结果:
========================================================================== test session starts ===========================================================================
platform win32 -- Python 3.13.6, pytest-9.0.2, pluggy-1.6.0
collected 2 items
cases\test_a.py F. [100%]
================================================================================ FAILURES ================================================================================
_______________________________________________________________________________ test_case1 _______________________________________________________________________________
args = (), kwargs = {}, executor = <concurrent.futures.thread.ThreadPoolExecutor object at 0x0000023769D74AD0>
future = <Future at 0x23769d74d70 state=finished returned NoneType>, error_msg = '测试用例 test_case1 已执行超过 3 秒。'
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(func, *args, **kwargs)
try:
return future.result(timeout=seconds)
except concurrent.futures.TimeoutError:
error_msg = f"测试用例 {func.__name__} 已执行超过 {seconds} 秒。"
> raise AssertionError(error_msg) from None
E AssertionError: 测试用例 test_case1 已执行超过 3 秒。
common\timeout.py:40: AssertionError
======================================================================== short test summary info =========================================================================
FAILED cases/test_a.py::test_case1 - AssertionError: 测试用例 test_case1 已执行超过 3 秒。
====================================================================== 1 failed, 1 passed in 8.11s =======================================================================
自定义 mark 标记
在 pytest 的配置文件(pytest.toml、pytest.ini)中自定义 mark 标记:
[pytest]
markers =
smoke: 标记冒烟测试用例
process: 标记业务流程测试
character_values_and_boundaries: 标记字符值及边界测试
[pytest]
markers = [
"smoke: 标记冒烟测试用例",
"process: 标记业务流程测试",
"character_values_and_boundaries: 标记字符值及边界测试"]
将 pytest 配置文件放在项目根目录下:
接口自动化测试框架/
├── business/ # 业务逻辑层(接口封装层,解耦用例与接口细节)
│ └── __init__.py
├── common/ # 【核心】公共工具模块(复用性代码,核心层)
│ └── __init__.py # 标记为Python包
├── cases/ # 自动化用例目录(仅调用业务层,不写具体逻辑)
│ └── __init__.py
├── config/ # 配置文件目录(与代码解耦,环境切换核心)
│ └── __init__.py
├── data/ # 测试数据目录(数据驱动,与用例分离)
├── docs/ # 项目文档目录(团队协作必备)
│ ├── api_docs.md # 接口文档(地址、参数、请求方式、响应示例)
│ └── usage_guide.md # 使用指南(环境搭建、运行命令、问题排查)
├── pytest.toml # pytest 配置文件
├── run.py # 项目启动文件(入口)
├── requirements.txt # 项目依赖包(指定版本,避免环境问题)
└── README.md # 项目说明(必选,快速上手:环境、运行、目录说明)
使用方法与上述官方标记的用法一样,而且还支持自定义参数传递,用于更精细的控制:
import pytest
@pytest.mark.smoke
def test_case1():
assert [1]
@pytest.mark.smoke
@pytest.mark.process(serial=1) # serial 为自定义参数,比如 serial=1 代表购物流程
def test_case2():
assert [1]
@pytest.mark.character_values_and_boundaries
def test_case3():
assert [2]
运行标记为 smoke 的测试用例:
python run.py -m smoke -v
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 3 items / 1 deselected / 2 selected
cases/test_a.py::test_case1 PASSED [ 50%]
cases/test_a.py::test_case2 PASSED [100%]
================================================== 2 passed, 1 deselected in 0.01s ==================================================
运行除了标记为 smoke 外的所有测试用例:
python run.py -m "not smoke" -v
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 3 items / 2 deselected / 1 selected
cases/test_a.py::test_case3 PASSED [100%]
================================================== 1 passed, 2 deselected in 0.01s ==================================================
运行标记为 process(serial=1) 的测试用例:
python run.py -m "process(serial=1)" -v
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 3 items / 2 deselected / 1 selected
cases/test_a.py::test_case2 PASSED [100%]
================================================== 1 passed, 2 deselected in 0.01s ==================================================
分别运行标记为 smoke 和标记为 character_values_and_boundaries 的测试用例:
python run.py -m "smoke or character_values_and_boundaries" -v
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 3 items
cases/test_a.py::test_case1 PASSED [ 33%]
cases/test_a.py::test_case2 PASSED [ 66%]
cases/test_a.py::test_case3 PASSED [100%]
========================================================= 3 passed in 0.01s =========================================================
运行同时标记为 smoke 和 process(serial=1) 的测试用例:
python run.py -m "smoke and process(serial=1)" -v
运行结果:
======================================================== test session starts ========================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 3 items / 2 deselected / 1 selected
cases/test_a.py::test_case2 PASSED [100%]
================================================== 1 passed, 2 deselected in 0.01s ==================================================
THEEND
© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0