
之前没讨论的 @pytest.mark.parametrize,会在这一篇详细阐述。@pytest.mark.parametrize 是 pytest 提供的用于参数化的标记器,在单独测试用例下需要不同测试数据的场景下使用。比如,参数的字符类型与长度测试、唯一值测试等等。
PS: 本文基于pytest 8.3.3
参数说明
@pytest.mark.parametrize 需要接收两个参数。第一个参数是字符串,里面定义参数变量;第二个参数是一个列表,列表内值是多个元组,元组的数量即是测试用例的执行次数。元组内的值需要与第一个参数字符串定义的变量对应。
比如 @pytest.mark.parametrize(“n,n1,n2”, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]),被装饰得测试用例会被执行三次。
第一次执行, 1 会传给 n,2 会传给 n1,3 会传给 n3;第二次执行, 3 会传给 n,4 会传给 n1,5 会传给 n3;第三次执行, 5 会传给 n,6 会传给 n1,7 会传给 n3。
被装饰的测试用例需要定义参数为装饰器第一个参数的变量。看下面示例。
参数化测试用例
# \Test\test_mod1.py
import pytest
@pytest.mark.parametrize("n, expected", [
(1 + 2, 3),
(2 + 2, 4),
(3 * 3, 9)
])
def test_math(n, expected):
assert n == expected
运行结果
===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items
Config::test_math[3-3] PASSED [ 33%]
Config::test_math[4-4] PASSED [ 66%]
Config::test_math[9-9] PASSED [100%]
====================================================================== 3 passed in 0.01s =======================================================================
参数化测试组
# \Test\test_mod1.py
import pytest
@pytest.mark.parametrize("n, expected", [(1, 2), (3, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
运行结果:
===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 4 items
Config::TestClass::test_simple_case[1-2] PASSED [ 25%]
Config::TestClass::test_simple_case[3-4] PASSED [ 50%]
Config::TestClass::test_weird_simple_case[1-2] PASSED [ 75%]
Config::TestClass::test_weird_simple_case[3-4] PASSED [100%]
====================================================================== 4 passed in 0.01s =======================================================================
参数化模块中测试用例
分配给 pytestmark 全局变量,还可以参数化模块中的所有测试:
# \Test\test_mod1.py
import pytest
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (2, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
def test_one(n, expected):
assert n+n == expected
def test_two(n, expected):
assert n+n == expected
运行结果:
===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 8 items
Config::TestClass::test_simple_case[1-2] PASSED [ 12%]
Config::TestClass::test_simple_case[2-4] FAILED [ 25%]
Config::TestClass::test_weird_simple_case[1-2] PASSED [ 37%]
Config::TestClass::test_weird_simple_case[2-4] FAILED [ 50%]
Config::test_one[1-2] PASSED [ 62%]
Config::test_one[2-4] PASSED [ 75%]
Config::test_two[1-2] PASSED [ 87%]
Config::test_two[2-4] PASSED [100%]
=================================================================== short test summary info ====================================================================
FAILED Config::TestClass::test_simple_case[2-4] - assert (2 + 1) == 4
FAILED Config::TestClass::test_weird_simple_case[2-4] - assert ((2 * 1) + 1) == 4
================================================================= 2 failed, 6 passed in 0.02s ==================================================================
组合参数化
还可以堆叠 parametrize装饰器,获取多个参数的所有组合:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
运行结果:
===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 4 items
Config::test_foo[2-0] PASSED [ 25%]
Config::test_foo[2-1] PASSED [ 50%]
Config::test_foo[3-0] PASSED [ 75%]
Config::test_foo[3-1] PASSED [100%]
====================================================================== 4 passed in 0.02s =======================================================================
会按照 x=0/y=2
x=1/y=2
x=0/y=3
x=1/y=3
的顺序组合参数。
参数化中应用标记
还可以在 parametrize 中标记单个测试实例,例如自定义的user:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.parametrize(
"n,expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*7", 42, marks=pytest.mark.user)],
)
def test_eval(n, expected):
assert eval(n) == expected
运行 python app.py Test/test_mod1.py -p v -tb no -m user,结果:
===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items / 2 deselected / 1 selected
Config::test_eval[6*7-42] PASSED [100%]
=============================================================== 1 passed, 2 deselected in 0.01s ================================================================
读取 json 文件进行参数化
在 @pytest.mark.parametrize 装饰器里设计参数化数据,终究太麻烦了。使用表格和 json 文件承载参数数据是最常用的方法,我们可以编写代码来支持从表格和 json 文件中读取参数进行参数化。
在 Package/ 目录下新增 parametrize_tools.py 模块,根目录下新增 ParametrizeData 目录,存放参数化数据文件:
Project/
│
├── Config/
│ └── pytest.ini # 自定义 mark 标记
│
├── ParametrizeData/
│ ├── user_add.json # user_add测试用例的参数化数据
│ └── user_delete.json # user_delete测试用例的参数化数据
│
├── Package/ # 程序目录
│ ├── __init__.py # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│ ├── timeout.py # 工具模块,定义了超时标记
│ ├── tools.py # 工具模块,包括处理参数化文件逻辑
│ ├── module1.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│ └── module2.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│
├── Test/ # 测试用例目录
│ ├── __init__.py # 包初始化文件
│ ├── test_module1.py # 测试 module1 的测试用例
│ └── test_module2.py # 测试 module2 的测试用例
├── app.py # 项目启动文件
├── requirements.txt # 项目依赖项列表
└── README.md # 项目说明文档
json 格式文件中,可以这样设计不带标记的数据,比如 user_add.json:
{
"param": "x,y,z",
"data": [
[1,8,9],
[2,5,7],
[11,9,20]
]
}
json 格式文件中,可以这样设计带标记的数据,比如 user_delete.json:
{
"param": "x,y,z",
"value": [
[1,8,9],
[2,5,7],
[11,9,20]
],
"mark": {
"user": 1
}
}
“user”: 1, 代表第二组数据-[2,5,7],被标记为 user 。
根据设计好的数据结构,在 tools.py 模块中,编写实现读取 json 文件进行参数化的处理器:
# tools.py
import json
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Tuple, List, Any, Optional, Dict
import pytest
# 定义文件处理策略接口
class FileStrategy(ABC):
@abstractmethod
def _read(self, path: str) -> Any:
"""Read data from a file and return parsed content."""
pass
def parametrize_data(self, path: str) -> Tuple[str, List[Tuple]]:
"""
Read data and return (param, data).
This can be overridden in subclasses if needed.
"""
parametrized_data = self._read(path)
if not isinstance(parametrized_data, dict):
raise ValueError("Expected dictionary format from file.")
if 'param' not in parametrized_data:
raise ValueError("Missing 'param' field in JSON.")
if 'value' not in parametrized_data:
raise ValueError("Missing 'data' field in JSON.")
# 获取参数和数据
parametrized_param = parametrized_data['param']
parametrized_values = [tuple(item) for item in parametrized_data['value']]
if 'mark' in parametrized_data:
for key, value in parametrized_data['mark'].items():
# 动态获取 pytest.mark 对象
mark_obj = getattr(pytest.mark, key)
# 将标记添加到数据中
parametrized_values[value] = pytest.param(*parametrized_values[value], marks=mark_obj)
return parametrized_param, parametrized_values
# JSON 文件处理器
class JsonHandler(FileStrategy):
def _read(self, path: str) -> Dict:
"""Read data from a JSON file and return parsed dict."""
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"File not found: {path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format in file: {path}") from e
# 表格文件处理器
class XlsxHandler(FileStrategy):
def _read(self, path: str) -> Dict:
"""Read data from a JSON file and return parsed dict."""
# 实现表格文件处理器
pass
# 文件处理器工厂
class HandlerFactory:
@staticmethod
def get_handler(file_path: str) -> FileStrategy:
ext = Path(file_path).suffix.lower()
if ext == '.json':
return JsonHandler()
# 后续可以轻松扩展支持 .yaml / .csv / .xlsx等
raise ValueError(f"Unsupported file type: {ext}")
# 处理器客户端
class Processor:
DATA_DIR = "ParametrizeData"
@classmethod
def _get_file_path(cls, file_name: str) -> str:
base_dir = Path(__file__).resolve().parent.parent
file_path = base_dir / cls.DATA_DIR / file_name
return str(file_path)
def parametrize_file(self, file_name: Optional[str]) -> Tuple[str, List[Tuple]]:
if file_name is None:
raise ValueError("File name cannot be None.")
file_path = self._get_file_path(file_name)
handler = HandlerFactory.get_handler(file_path)
return handler.parametrize_data(file_path)
# 测试主函数
if __name__ == "__main__":
try:
processor = Processor()
param, data = processor.parametrize_file('user_add.json')
print("Parameters:", param)
print("ParametrizeData:", data)
print("First data item second value:", data[0][1])
except Exception as e:
print(f"[Error] {e}")
测试用例中使用编写好的参数化处理器:
# \Test\test_mod1.py
import pytest
from Package.tools import Processor
# 实例化处理器
processor = Processor()
param, data = processor.parametrize_file('user_add.json')
@pytest.mark.parametrize(param,data)
def test_one(x,y,z):
assert x+y == z
param, data = processor.parametrize_file('user_delete.json')
@pytest.mark.parametrize(param,data)
def test_two(x,y,z):
assert x+y > z
python app.py Test/test_mod1.py -p v -tb no,运行结果:
============================================================================================== test session starts ==============================================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 6 items
Config::test_one[1-8-9] PASSED [ 16%]
Config::test_one[2-5-7] PASSED [ 33%]
Config::test_one[11-9-20] PASSED [ 50%]
Config::test_two[1-8-9] FAILED [ 66%]
Config::test_two[2-5-7] FAILED [ 83%]
Config::test_two[11-9-20] FAILED [100%]
============================================================================================ short test summary info ============================================================================================
FAILED Config::test_two[1-8-9] - assert (1 + 8) > 9
FAILED Config::test_two[2-5-7] - assert (2 + 5) > 7
FAILED Config::test_two[11-9-20] - assert (11 + 9) > 20
========================================================================================== 3 failed, 3 passed in 0.02s ==========================================================================================
python app.py Test/test_mod1.py -p v -tb no -m user ,运行结果:
============================================================================================== test session starts ==============================================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 6 items / 5 deselected / 1 selected
Config::test_two[2-5-7] FAILED [100%]
============================================================================================ short test summary info ============================================================================================
FAILED Config::test_two[2-5-7] - assert (2 + 5) > 7
======================================================================================== 1 failed, 5 deselected in 0.02s ========================================================================================
如果需要其它类型文件装载参数化数据,可以在 tools.py 中增加对应类型文件处理逻辑。比如想要从 .xlsx 文件中读取,就可以新建 XlsxHandler 类并构建 _read 方法,确保返回的数据是 dict 类型。
参数化中使用 Fixture
用法与一般的参数化一致,只是第一个参数字符串中写成 Fixture 函数名称。
# \Test\test_mod1.py
import pytest
@pytest.fixture()
def one_param_fixture(request):
"""
通过 request.param 获取参数值
"""
return request.param
@pytest.fixture()
def one_param_fixture2(request):
"""
通过 request.param 获取参数值
"""
return request.param
@pytest.mark.parametrize("one_param_fixture,one_param_fixture2", [(2, 3), (4, 5)])
def test_one_param_fixture(one_param_fixture, one_param_fixture2):
assert one_param_fixture*one_param_fixture2 == 6, "测试失败"
运行结果:
============================================================================================== test session starts ==============================================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 2 items
Config::test_one_param_fixture[2-3] PASSED [ 50%]
Config::test_one_param_fixture[4-5] FAILED [100%]
============================================================================================ short test summary info ============================================================================================
FAILED Config::test_one_param_fixture[4-5] - AssertionError: 测试失败
========================================================================================== 1 failed, 1 passed in 0.01s ==========================================================================================
THEEND

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