
mark 标记是 pytest 的特点之一,功能丰富且灵活。官方不仅内置了一些标记,还允许用户创建自定义标记,使用自定义标记来分类测试并运行指定分类测试用例。
PS: 本文基于pytest 8.3.3。
自定义 mark 标记
需要在 pytest.ini 文件中自定义 mark 标记,可以如下:
[pytest]
markers =
smoke: 标记冒烟测试用例
process: 标记业务流程测试
priority: 标记优先级。1代表最高优先级,数字越大优先级越小
success: 标记成功用例
failed: 标记失败用例
character_values_and_boundaries: 标记字符值及边界测试
除了在 pytest.ini 自定义外,还有其它方法,这里就不介绍了,感兴趣可以访问官方文档:Working with custom markers。
使用自定义标记
使用自定义标记测试用例
- 标记单独测试用例:
# test_module1.py
import pytest
@pytest.mark.smoke
def test_case1():
assert [1]
@pytest.mark.smoke
@pytest.mark.process(serial=1) # serial为1代表购物流程
def test_case2():
assert [1]
@pytest.mark.process(serial=1)
def test_case3():
assert [2]
- 标记测试组
# test_module1.py
import pytest
@pytest.mark.success
class TestCase:
def test_case4(self):
assert [1]
def test_case5(self):
assert [1]
def test_case6(self):
assert [2]
- 标记整个模块
# test_module2.py
import pytest
# 标记本模块
pytestmark = pytest.mark.failed
def test_case7():
assert [1]
def test_case8():
assert [1]
class TestCase:
def test_case9(self):
assert [1]
def test_case10(self):
assert [1]
def test_case11(self):
assert [2]
运行指定标记的测试用例
必须指定 pytest.ini 文件的路径(绝对路径或相对路径)
- 运行标记为 smoke 的测试用例
pytest -v C:\PythonTest\Test\ -c "Config/pytest.ini" -m smoke
运行结果:
============================ test session starts =============================
configfile: pytest.ini
collected 11 items / 9 deselected / 2 selected
Config\test_mod1.py::test_case1 PASSED [ 50%]
Config\test_mod1.py::test_case2 PASSED [100%]
====================== 2 passed, 9 deselected in 0.01s =======================
- 运行除了标记为 smoke 外的所有测试用例
pytest -v C:\PythonTest\Test\ -c "Config/pytest.ini" -m "not smoke"
运行结果:
============================ test session starts =============================
configfile: pytest.ini
collected 11 items / 2 deselected / 9 selected
Config\test_mod1.py::test_case3 PASSED [ 11%]
Config\test_mod1.py::TestCase::test_case4 PASSED [ 22%]
Config\test_mod1.py::TestCase::test_case5 PASSED [ 33%]
Config\test_mod1.py::TestCase::test_case6 PASSED [ 44%]
Config\test_mod2.py::test_case7 PASSED [ 55%]
Config\test_mod2.py::test_case8 PASSED [ 66%]
Config\test_mod2.py::TestCase::test_case9 PASSED [ 77%]
Config\test_mod2.py::TestCase::test_case10 PASSED [ 88%]
Config\test_mod2.py::TestCase::test_case11 PASSED [100%]
====================== 9 passed, 2 deselected in 0.01s =======================
- 运行标记为 process(serial=1) 的测试用例
pytest -v C:\PythonTest\Test\ -c "Config/pytest.ini" -m "process(serial=1)"
运行结果:
============================ test session starts =============================
configfile: pytest.ini
collected 11 items / 9 deselected / 2 selected
Config\test_mod1.py::test_case2 PASSED [ 50%]
Config\test_mod1.py::test_case3 PASSED [100%]
====================== 2 passed, 9 deselected in 0.01s =======================
- 分别运行标记为 smoke 和标记为 success 的测试用例
pytest -v C:\PythonTest\Test\ -c "Config/pytest.ini" -m "smoke or success"
运行结果:
============================ test session starts =============================
configfile: pytest.ini
collected 11 items / 6 deselected / 5 selected
Config\test_mod1.py::test_case1 PASSED [ 20%]
Config\test_mod1.py::test_case2 PASSED [ 40%]
Config\test_mod1.py::TestCase::test_case4 PASSED [ 60%]
Config\test_mod1.py::TestCase::test_case5 PASSED [ 80%]
Config\test_mod1.py::TestCase::test_case6 PASSED [100%]
====================== 5 passed, 6 deselected in 0.01s =======================
- 运行同时标记为 smoke 和 process(serial=1) 的测试用例
pytest -v C:\PythonTest\Test\ -c "Config/pytest.ini" -m "smoke and process(serial=1)"
运行结果:
============================ test session starts =============================
configfile: pytest.ini
collected 11 items / 10 deselected / 1 selected
Config\test_mod1.py::test_case2 PASSED [100%]
====================== 1 passed, 10 deselected in 0.01s ======================
传递参数给自定义标记
pytest 支持自定义标记接受位置参数和关键字参数,上文的 @pytest.mark.process(serial=1) 中 serial=1 就是关键字参数。
虽然可以传递函数对象等复杂类型,但推荐使用基本数据类型(字符串、数字等),只做标记用。
位置参数
使用方法如下:
import pytest
# 自定义标记
@pytest.mark.smoke(1, 2, 3)
def test_example(request):
# 传递给 get_closest_marker() 的参数应该是标记名称
marker = request.node.get_closest_marker("smoke")
if marker:
print(marker.args) # 输出: (1, 2, 3)
print(marker.args[0]) # 输出: 1
print(marker.args[1]) # 输出: 2
print(marker.args[2]) # 输出: 3
assert True
关键词参数
import pytest
# 自定义标记
@pytest.mark.smoke(a=1, b=2, c=3)
def test_example(request):
marker = request.node.get_closest_marker("smoke")
if marker:
print(marker.kwargs) # 输出: {'a': 1, 'b': 2, 'c': 3}
assert True
官方标记 mark.skip
@pytest.mark.skip 是 pytest 框架中用于跳过测试用例的官方标记。它允许开发者在特定条件下(如功能未实现、环境不支持等)跳过测试用例的执行,并在测试报告中显示跳过原因。
无条件跳过测试用例
对于暂时不会执行的测试用例,可以使用 @pytest.mark.skip 装饰:
- 跳过单个测试用例
import pytest
@pytest.mark.skip(reason="功能尚未实现")
def test_example():
assert False # 该测试不会执行
运行结果:
============================ test session starts =============================
collected 1 item
Test/test_mod1.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
Test/test_mod1.py::TestSkipClass::test_case1 SKIPPED (整个测试组暂不执行) [ 50%]
Test/test_mod1.py::TestSkipClass::test_case2 SKIPPED (整个测试组暂不执行) [100%]
============================= 2 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
Test/test_mod1.py::test_case1 SKIPPED (模块暂不执行) [ 50%]
Test/test_mod1.py::test_case2 SKIPPED (模块暂不执行) [100%]
============================= 2 skipped in 0.01s =============================
跳过满足特定条件的测试用例
pytest.mark.skipif 常用与跳过特定平台的测试用例,同样可以装饰单独测试用例、测试组、模块文件。不过应用场景比较少。
示例跳过win32环境(针对测试机):
import pytest
import sys
# 定义通用标记:在 Windows 平台跳过
@pytest.mark.skipif(
sys.platform == "win32",
reason="不支持 Windows"
)
def test_case():
assert True
动态跳过用例
相较于 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
Test/test_mod1.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
Test/test_mod1.py::TestDynamicSkip::test_case1 SKIPPED (不满足条件,跳过此测试组) [ 50%]
Test/test_mod1.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
Test/test_mod1.py::test_case1 SKIPPED (不满足条件,模块暂不执行) [ 50%]
Test/test_mod1.py::test_case2 SKIPPED (不满足条件,模块暂不执行) [100%]
============================= 2 skipped in 0.01s =============================
- 跳过测试套件
用例目录里的 init.py 文件里定义:
# 定义条件函数
def condition():
return True # 根据实际需求返回 True 或 False
if condition():
pytestmark = pytest.mark.skip(reason="不满足条件,未执行测试")
满足条件后,会跳过这个目录下的所有测试用例。
官方标记 mark.xfail
它可以标记测试用例为预期失败,用于已经知道测试的功能存在问题,进行标记。当断言失败时,不会计入失败;如果断言意外通过成功,可以设置 strict=True 来将其标记为失败。
import pytest
@pytest.mark.xfail(reason="这个测试用例应该不通过", run=True)
def test_case1():
assert False
@pytest.mark.xfail(reason="这个测试用例应该不通过")
def test_case2():
assert False
运行结果:
============================ test session starts =============================
collected 3 items
Test/test_mod1.py::test_case1 XFAIL (这个测试用例应该不通过) [ 33%]
Test/test_mod1.py::test_case2 XPASS (这个测试用例应该不通过) [ 66%]
Test/test_mod1.py::test_case3 FAILED [100%]
================================== FAILURES ==================================
_________________________________ test_case3 _________________________________
[XPASS(strict)] 这个测试用例应该不通过
========================== short test summary info ===========================
FAILED Test/test_mod1.py::test_case3
================== 1 failed, 1 xfailed, 1 xpassed in 0.05s ===================
编写超时处理标记
本来应该介绍第三方标记 @pytest.mark.timeout,对测试用例设置超时时间。但是此插件在 Windows 平台不会按预期运行,我也不使用 Linux 平台,所以就不介绍它了。读者如果有需求,可以查看pytest-timeout文档。
如果一个测试用例的执行时间超过我的预期时间,则视为失败,这是我的需求。我完全可以实现一个简单的装饰器满足需求。
- 在 Package/ 目录下新建 timeout.py 文件
# Package/tool.py
import concurrent.futures
import functools
# 定义超时装饰器,接收参数 seconds(默认10秒),返回一个装饰器函数 decorator
def timeout(seconds: int=10)->callable:
"""
:param seconds:
:return: decorator
"""
# 接收被装饰的函数 func,返回一个包装函数 wrapper。
def decorator(func:callable)->callable:
@functools.wraps(func) # 拷贝被装饰函数的元信息
def wrapper(*args, **kwargs)-> any: # 定义包装函数及超时逻辑
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(func)
try:
return future.result(timeout=seconds)
except concurrent.futures.TimeoutError:
# 抛出 AssertionError 并携带自定义错误信息
error_msg = f"测试用例 {func.__name__} 已执行超过 {seconds} 秒。"
raise AssertionError(error_msg) from None
return wrapper
return decorator
- 使用 @timeout 装饰器
# Test/test_mod1.py
import requests
from Package.timeout import timeout
@timeout(3)
def test_mod1():
# 想请求 http://127.0.0.1:5000/TimeOut/?sleep=5 接口,可以在 Requests 系列文章中下载
# sleep=x代表接口x秒后响应,x需要是数字,比如5、6、11、7等等
result = requests.get("http://127.0.0.1:5000/TimeOut/?sleep=6")
assert result.status_code == 200, "失败"
@timeout(3)
def test_mod2():
result = requests.get("http://127.0.0.1:5000/TimeOut/?sleep=2")
assert result.status_code == 200, "失败"
- 运行结果
===================================================================== test session starts ======================================================================
collected 2 items
Test/test_mod2.py::test_mod1 FAILED [ 50%]
Test/test_mod2.py::test_mod2 PASSED [100%]
=================================================================== short test summary info ====================================================================
FAILED Test/test_mod2.py::test_mod1 - AssertionError: 测试用例 test_mod1 已执行超过 3 秒。
================================================================= 1 failed, 1 passed in 8.29s ==================================================================
第三方标记 mark.order
pytest-order 是 pytest-ordering 的分支,比较新。它可以指定测试用例的执行顺序。
安装 pytest-order :
pip install pytest-order
pytest 框架中,测试用例的定义顺序就是执行顺序(包括模块),比如:
# test_mod1.py
def test_case1():
assert True
def test_case2():
assert True
def test_case3():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 3 items
Test/test_mod1.py::test_case1 PASSED [ 33%]
Test/test_mod1.py::test_case2 PASSED [ 66%]
Test/test_mod1.py::test_case3 PASSED [100%]
====================================================================== 3 passed in 0.01s =======================================================================
使用 @pytest.mark.order() 标记,可以改变测试用例的执行顺序,比如:
# test_mod1.py
import pytest
@pytest.mark.order(3)
def test_case1():
assert True
@pytest.mark.order(1)
def test_case2():
assert True
@pytest.mark.order(2)
def test_case3():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 3 items
Test/test_mod1.py::test_case2 PASSED [ 33%]
Test/test_mod1.py::test_case3 PASSED [ 66%]
Test/test_mod1.py::test_case1 PASSED [100%]
====================================================================== 3 passed in 0.01s =======================================================================
按索引排序
与 python 索引的规则一致,比如 -1 代表最后一个执行,-2 代表倒数第二个执行,0 代表第一个执行,1 代表第二个执行。
# test_mod1.py
import pytest
@pytest.mark.order(-2)
def test_three():
assert True
@pytest.mark.order(-1)
def test_four():
assert True
@pytest.mark.order(2)
def test_two():
assert True
@pytest.mark.order(1)
def test_one():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 4 items
Test/test_mod1.py::test_one PASSED [ 25%]
Test/test_mod1.py::test_two PASSED [ 50%]
Test/test_mod1.py::test_three PASSED [ 75%]
Test/test_mod1.py::test_four PASSED [100%]
====================================================================== 4 passed in 0.01s =======================================================================
相对于其他测试用例的顺序
主要是两个参数:after(之后执行) 和 before(之前执行),如下:
# test_mod1.py
import pytest
@pytest.mark.order(after="test_second")
def test_third():
assert True
def test_second():
assert True
@pytest.mark.order(before="test_second")
def test_first():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 3 items
Test/test_mod1.py::test_first PASSED [ 33%]
Test/test_mod1.py::test_second PASSED [ 66%]
Test/test_mod1.py::test_third PASSED [100%]
====================================================================== 3 passed in 0.01s =======================================================================
还有其它用法,感兴趣者可以查阅pytest-order文档。
提醒一下,不要滥用 order 标记,非必要不使用。
第三方标记mark.dependency
它可以定义测试用例的依赖项。简单来说,如果被依赖的测试用例未通过(状态不是passed),则设置依赖项的测试用例会被跳过。
安装 pytest-dependency:
pip install pytest-dependency
基本使用方法:
# test_mod1.py
import pytest
# name可以为测试用例起别名,方便后续依赖
@pytest.mark.dependency(name="xfail")
@pytest.mark.xfail(reason="预期失败")
def test_xfail():
assert False
@pytest.mark.dependency(name="skip")
@pytest.mark.skip(reason="预期跳过")
def test_skip():
assert True
@pytest.mark.dependency(name="failed")
def test_failed():
assert False
@pytest.mark.dependency(name="passed")
def test_passed():
assert True
# depends 定义被依赖项,
@pytest.mark.dependency(depends=["xfail"])
def test_a():
assert True
@pytest.mark.dependency(depends=["skip"])
def test_b():
assert True
@pytest.mark.dependency(depends=["failed"])
def test_c():
assert True
# 只有被依赖项状态为passed,才会执行测试用例
@pytest.mark.dependency(depends=["passed"])
def test_d():
assert True
# 只有多个被依赖项状态同时为passed,才会执行测试用例
@pytest.mark.dependency(depends=["passed", "failed", "xfail", "skip"])
def test_e():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 9 items
Test/test_mod1.py::test_xfail XFAIL (预期失败) [ 11%]
Test/test_mod1.py::test_skip SKIPPED (预期跳过) [ 22%]
Test/test_mod1.py::test_failed FAILED [ 33%]
Test/test_mod1.py::test_passed PASSED [ 44%]
Test/test_mod1.py::test_a SKIPPED (test_a depends on xfail) [ 55%]
Test/test_mod1.py::test_b SKIPPED (test_b depends on skip) [ 66%]
Test/test_mod1.py::test_c SKIPPED (test_c depends on failed) [ 77%]
Test/test_mod1.py::test_d PASSED [ 88%]
Test/test_mod1.py::test_e SKIPPED (test_e depends on failed) [100%]
====================================================== 1 failed, 2 passed, 5 skipped, 1 xfailed in 0.02s =======================================================
还可以在测试组中使用及依赖的同时被依赖:
# test_mod1.py
import pytest
class Test:
@staticmethod
@pytest.mark.dependency(name="failed")
def test_failed():
assert False
@staticmethod
@pytest.mark.dependency(name="passed")
def test_passed():
assert True
class Test2:
@staticmethod
@pytest.mark.dependency(depends=["failed"])
def test_a():
assert True
@staticmethod
@pytest.mark.dependency(name="b",depends=["passed"])
def test_b():
assert False
@pytest.mark.dependency(depends=["b"])
def test_c():
assert True
运行结果:
===================================================================== test session starts ======================================================================
collected 5 items
Test/test_mod1.py::Test::test_failed FAILED [ 20%]
Test/test_mod1.py::Test::test_passed PASSED [ 40%]
Test/test_mod1.py::Test2::test_a SKIPPED (test_a depends on failed) [ 60%]
Test/test_mod1.py::Test2::test_b FAILED [ 80%]
Test/test_mod1.py::test_c SKIPPED (test_c depends on b) [100%]
============================================================ 2 failed, 1 passed, 2 skipped in 0.02s ============================================================
也可以跨模块使用,但是涉及的模块都要被执行:
# test_mod1.py
import pytest
@pytest.mark.dependency(name="failed")
def test_failed():
assert False
运行结果:
===================================================================== test session starts ======================================================================
collected 2 items
Test/test_mod1.py::test_failed FAILED [ 50%]
Test/test_mod2.py::test_01 SKIPPED (test_01 depends on failed) [100%]
================================================================= 1 failed, 1 skipped in 0.01s =================================================================
当然还有一些用法,感兴趣者可以查阅pytest-dependency文档。
而且,不要滥用 dependency 标记,非必要不使用。测试用例的独立性是非常必要的。
还有 @pytest.mark.parametrize 会在后面的篇章中介绍。除此之外,可以查阅pytest插件列表,有很多有用的插件。
补充测试套件
把 pytest.ini 放在 Config 目录下,并把自定义的超时标记放在 Package 目录下:
Project/
│
├── Config/
│ └── pytest.ini # 自定义 mark 标记
│
├── Package/ # 程序目录
│ ├── __init__.py # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│ ├── timeout.py # 工具模块,定义了超时标记
│ ├── module1.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│ └── module2.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│
├── Test/ # 测试用例目录
│ ├── __init__.py # 包初始化文件
│ ├── test_module1.py # 测试 module1 的测试用例
│ └── test_module2.py # 测试 module2 的测试用例
├── app.py # 项目启动文件
├── requirements.txt # 项目依赖项列表
└── README.md # 项目说明文档
在app.py中指定 pytest.ini 的路径,并新增 -m 命令行参数:
from typing import Union
import pytest
import sys
import logging
import argparse
import os
# 自定义日志格式
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
# 配置文件
CONFIG_PATH = "Config/pytest.ini"
def run_tests(test_target: str, testprint: str, tb: str, mark: str)->int:
"""
运行pytest测试并返回退出码
Args:
test_target (str): 测试目标路径
testprint (str): 控制输出信息的命令行参数
tb (str): 控制输出信息的命令行参数
mark (str): 指定运行特定标记的测试用例
Returns:
int: pytest退出码
"""
if not os.path.exists(test_target):
logger.error(f"测试目标路径不存在: {test_target}")
return 1
# 构建pytest参数
pytest_args = []
if os.path.exists(CONFIG_PATH):
pytest_args.append("-c")
pytest_args.append(CONFIG_PATH)
else:
logger.warning("未找到配置文件,将使用默认配置")
if testprint:
pytest_args.append("-"+ testprint)
pytest_args.extend([
test_target,
"--tb=" + tb
])
if mark:
pytest_args.append("-m")
pytest_args.append(mark)
try:
logger.info(f"开始运行测试,目标路径: {test_target}")
exit_code = pytest.main(pytest_args)
# 退出码说明映射
exit_messages = {
0: "✅ 全部测试用例通过",
1: "⚠️ 部分测试用例未通过",
2: "❌ 测试过程中有中断或其他非正常终止",
3: "❌ 内部错误",
4: "❌ pytest无法找到任何测试用例",
5: "❌ pytest遇到了命令行解析错误"
}
logger.info(exit_messages.get(exit_code, f"❓ 未知的退出码: {exit_code}"))
return exit_code
except Exception as e:
logger.exception("运行测试时发生致命错误:")
logger.debug("异常详情:", exc_info=True)
return 1
def parse_arguments()-> argparse.Namespace:
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="使用指定的命令运行 pytest 测试")
parser.add_argument(
'test_target',
nargs='?',
type=str,
default="Test/",
help='指定测试目录文件 (默认: Test/)'
)
parser.add_argument(
'-p', '--testprint',
nargs='?',
type=str,
default="",
choices=["","v", "ra", "rA", "rp", "rx", "rs", "rN", "vra", "vrA", "vrp", "vrx", "vrs", "vrN"],
help='控制输出信息'
)
parser.add_argument(
'-tb', '--tb',
nargs='?',
type=str,
default="auto",
choices=["auto","long", "short", "line", "native", "no"],
help='控制输出信息'
)
parser.add_argument(
'-m', '--mark',
nargs='?',
type=str,
help='控制输出信息'
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_arguments()
exit_code = run_tests(args.test_target, args.testprint, args.tb, args.mark)
sys.exit(exit_code)
项目根目录下,运行指定标记的测试用例只需要 -m + 参数即可。
THEEND

© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0