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