之前没讨论的 @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