把参数化数据放进文件中管理,是一个很好的需求,这样可以避免在代码中频繁改动。
确定文件格式和数据结构
我使用 toml 格式文件管理测试数据,没有写其他格式文件的逻辑。读者可以参考一下我的逻辑。
约定数据结构
主要约束三种数据结构:
-
单组测试数据:
[test_case_name] aaaa= "1" cccc= 2 eeee= 3 gggg= "4" iiii= "5" kkkk= "6"最终效果如:
pytest.mark.parametrize("aaaa, cccc, eeee, gggg, iiii, kkkk", [("1", 2, 3, "4", "5", "6")]) -
多组测试数据1:
[test_case_name] aaaa= "1" cccc= "2" eeee= 3 gggg= ["4", "5", 6] iiii= ["7", "8", 9] kkkk= "10"最终效果如:
pytest.mark.parametrize("aaaa, cccc, eeee, gggg, iiii, kkkk", [('1', '2', 3, '4', '7', '10'), ('1', '2', 3, '5', '8', '10'), ('1', '2', 3, 6, 9, '10')]) -
多组测试数据2:
[test_case_name] aaaa= [1, 2, "3"] cccc= [1, 2, "3"] eeee= [1, 2, "3"] gggg= [1, 2, "3"] iiii= [1, 2, "3"] kkkk= [1, 2, "3"]最终效果如:
pytest.mark.parametrize("aaaa,cccc,eeee,gggg,iiii,kkkk", [(1, 1, 1, 1, 1, 1), (2, 2, 2, 2, 2, 2), ('3', '3', '3', '3', '3', '3')])
逻辑实现
在 common/get_data.py 中实现
from pathlib import Path
import tomllib
import pytest
from typing import Callable
def load_toml(name: str) -> dict:
"""
加载指定名称的TOML配置文件并解析为字典。
参数:
name (str): 配置文件的名称(不包含.toml后缀),函数会自动拼接后缀并在项目根目录的config子目录中查找。
返回:
dict: 解析后的TOML配置数据,以字典形式返回,键值对应配置文件中的内容。
异常处理:
- FileNotFoundError: 当指定的配置文件在config目录中不存在时引发,包含具体的文件路径信息。
- tomllib.TOMLDecodeError: 当配置文件存在但TOML格式错误(如语法错误、结构异常)导致无法解析时引发,包含错误详情。
- RuntimeError: 当发生其他未知错误(如文件读取权限问题、意外异常)导致加载失败时引发,包含错误描述。
"""
root_directory = Path(__file__).resolve().parent.parent
name = name + ".toml"
# 定义文件路径
config_path = root_directory / "config" / name
data_path = root_directory / "data" / name
# 判断文件是否存在,如果都不存在,赋值为None
toml_file_path = config_path if config_path.is_file() else (data_path if data_path.is_file() else None)
try:
with open(str(toml_file_path), "rb") as f:
data = tomllib.load(f)
return data
except FileNotFoundError:
raise FileNotFoundError(f"配置文件不存在: {toml_file_path}")
except tomllib.TOMLDecodeError as e:
raise ValueError(f"配置文件格式错误: {str(e)}")
except Exception as e:
raise RuntimeError(f"加载配置失败: {str(e)}")
def parametrize(file_name: str, case_name: str) -> Callable :
"""
从TOML配置文件加载测试数据,生成pytest参数化装饰器,自动处理参数广播逻辑。
该函数作为pytest.mark.parametrize的封装,支持从指定TOML文件中读取指定测试用例的参数,
自动识别列表类型的参数字段:若存在列表字段,要求所有列表字段长度一致,并将非列表字段
广播(重复)至相同长度,最终生成符合pytest参数化要求的参数列表,实现测试函数的批量执行。
参数:
file_name (str): TOML配置文件路径(含文件名),例如"test_data.toml"
- 支持相对路径(基于当前工作目录)或绝对路径
- 要求文件格式合法,避免解析失败
case_name (str): 测试用例名称,对应TOML文件中的顶级节点名称
- 示例:若TOML中有[login_case]节点,case_name传入"login_case"
返回:
Callable: pytest参数化装饰器,可直接装饰测试函数,使测试函数按配置的参数批量执行
- 装饰器会将参数按字段顺序拼接为参数名字符串,参数值按行组织为元组列表
异常处理:
- ValueError: 配置校验失败时触发,包含三种场景:
- TOML文件中未找到指定名称的测试用例节点(case_name)
- 测试用例中存在多个列表类型字段,但各列表长度不一致
- 测试用例中列表类型字段为空列表(len(value)=0)
- tomlkit.exceptions.ParseError: TOML文件格式错误导致加载失败(由load_toml抛出)
- FileNotFoundError: 指定的TOML配置文件不存在(由load_toml抛出)
最佳实践建议:
- 配置文件规范:TOML文件中每个测试用例节点独立,字段命名与测试函数参数名保持一致
- 示例配置格式1:
[login_success]
username = ["user1", "user2", "user3"]
password = "123456"
expected = True
- 示例配置格式2:
[login_success]
username = "user1"
password = "123456"
expected = True
- 说明:username是列表字段(长度3),password/expected会自动广播为长度3的列表
- 列表字段约束:所有列表类型的测试数据字段长度必须完全一致,避免参数匹配错位
- 错误示例:username=["a","b"], age=[18] → 会抛出长度不一致异常
- 空列表规避:列表类型字段禁止为空(len(value)=0),否则会被过滤且可能导致无参数可用
- 参数名匹配:测试函数的参数名需与TOML中的字段名完全一致,否则pytest会报参数未定义错误
- 例如TOML有username/password字段,测试函数需定义为def test_login(username, password):
- 配置文件管理:建议将测试数据按模块拆分到不同TOML文件,避免单个文件过大,便于维护
- 示例:用户模块测试数据放在test_user.toml,订单模块放在test_order.toml
示例:
@parametrize("test_data.toml", "login_case")
def test_login(...): ...
"""
toml_data = load_toml(file_name)
section = toml_data.get(case_name, {})
# 没有没有测试数据,会报错
if not section:
raise ValueError(f"配置文件中未找到测试数据:{case_name}")
# 提取参数名称
field_order = list(section.keys())
# 提取值为列表的参数,没有列表即单组数据
list_fields = {key for key, value in section.items()
if isinstance(value, list) and len(value) > 0}
# 如果是单组数据,直接返回装饰器
if not list_fields:
def decorator(func: Callable) -> Callable:
return pytest.mark.parametrize(",".join(field_order), [tuple(section[key] for key in field_order)])(func)
return decorator
# 如果是多组数据,继续处理
# 提取各列表的长度,存进集合
lengths = {len(section[key]) for key in list_fields}
# 如果长度不一致,报错
if len(lengths) != 1:
length_map = {key: len(section[key]) for key in list_fields}
raise ValueError(f"测试数据中列表字段长度不一致:{length_map}")
# 如果长度一致,继续处理
n = next(iter(lengths))
# 处理数据,提取各列表的值,存进字典
broadcasted = {}
for key in field_order:
if key in list_fields:
broadcasted[key] = section[key]
else:
broadcasted[key] = [section[key]] * n
# 返回装饰器
def decorator(func: Callable) -> Callable:
return pytest.mark.parametrize(",".join(field_order), [tuple(broadcasted[key][i] for key in field_order) for i in range(n)])(func)
return decorator
使用自定义 parametrize 装饰器
拿单组测试测试示例
测试函数参数的从左到右的顺序一定要与数据文件中的从上到下顺序一致。
from common.get_data import parametrize
@parametrize("role_permissions", "test_add_role")
def test_aa(aaaa,cccc,eeee,gggg,iiii,kkkk):
assert aaaa == "1"
assert cccc == 2
assert eeee == 3
assert gggg == "4"
assert iiii == "5"
assert kkkk == "6"
运行结果:
==================================================== test session starts =====================================================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
configfile: pytest.toml
collected 1 item
test/test_aaaaa.py . [100%]
===================================================== 1 passed in 0.02s ======================================================
读者可以参考我的实现逻辑,实现自己的需求。
THEEND
© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0