引言:为什么我们要抛弃 “手写用例”?
在接口自动化实践中,当项目规模扩大、用例数量激增时,传统“手写Pytest用例”的模式往往会陷入瓶颈。
做接口自动化的同学,大概率都踩过这样的硬编码坑:写一条 “新增 - 查询 - 删除” 的流程用例,要重复写 3 个接口的请求、参数与断言代码;不同同事写的用例,有的把数据塞代码里,有的存 Excel,交接时看得头大;新手没代码基础,想加个用例还要先学 Python 语法。
一、遇到的3个核心痛点
我们公司在维护Pytest接口自动化项目时,深刻感受到手写用例带来的诸多困扰,随着项目规模扩大,问题愈发凸显:
- 用例编写效率低,重复劳动多。一条流程用例要调用多个接口,每个接口的请求头、参数、断言都要手写,浪费时间。
- 代码混乱无规范,维护成本高。测试同学各自为战,测试数据存储方式不一样(硬编码、data.py、Excel等);并且重复编写“发送请求”“数据库查询”等通用功能,导致项目冗余代码堆积,新人接手时难以梳理逻辑。
- 门槛高,新手难上手。无Python基础的测试同学,需先学习requests库、Pytest语法、断言写法等技术内容,再结合混乱的项目结构,入门难度大,难以快速参与用例编写。
二、核心解决方案:数据与逻辑分离,自动生成测试用例
针对上述痛点,我们提出核心解决方案:测试人员仅负责“设计测试数据”(基于YAML),用例生成器自动完成“用例代码编写”,通过“数据与逻辑分离”的思路,从根源解决手写用例的弊端。
1. 核心设计思路
- 把 “测试数据” 和 “用例逻辑” 彻底分开,使数据与逻辑解耦。将接口参数、断言规则、前置后置操作等测试数据,按约定格式存入YAML文件,测试人员无需关注代码逻辑,专注业务数据设计。
- 自动生成 Pytest 测试用例文件。定义一个用例生成器模块,,读取YAML文件中的测试数据,自动校验格式并生成标准化的Pytest用例代码,完全替代手写用例。
2. 方案核心优势
- 零代码门槛:测试人员无需编写Python代码,只需按模板填写YAML,降低技术要求。
- 输出标准化:生成的用例命名、目录结构、日志格式、断言方式完全统一,告别代码混乱。
- 批量高效生成:支持整个目录的 YAML 文件批量生成,一次生成上百条用例;
- 零维护成本:接口变更时,只改 YAML 数据,生成器重新运行即可更新用例。
3. 完整实施流程
完整流程为:编写YAML测试数据 → 运行生成器自动生成测试用例 → 执行自动生成的Pytest用例
三、关键步骤:从 YAML 设计到自动生成用例
下面通过“实操步骤+代码示例”的方式,详细说明方案的落地过程,以“新增设备→查询设备→解绑设备”的完整流程用例为例。
第一步:设计标准化YAML测试数据格式
YAML文件是方案的核心,需兼顾“完整性”与“易用性”,既要覆盖接口测试的全场景需求,又要让测试人员容易理解和填写。
我们设计的YAML格式支持:基础信息配置、前置/后置操作、多接口步骤串联、多样化断言(常规断言+数据库断言)。
YAML示例如下(test_device_bind.yaml):
/* by yours.tools - online tools website : yours.tools/zh/iq.html */
# test_device_bind.yaml
testcase:
name: bind_device # 用例唯一标识,建议和文件名一致(去掉test_)
description: 新增设备→查询设备→解绑设备 # 用例说明,清晰易懂
allure: # Allure报告配置,方便统计
epic: 商家端
feature: 设备管理
story: 新增设备
setups: # 前置操作:执行测试前的准备(如数据库查询、数据初始化)
- id: check_database
description: 检查设备是否已存在
operation_type: db # 操作类型:db=数据库操作
query: SELECT id FROM device WHERE imei = '865403062000000'
expected: id # 预期查询结果存在id字段
steps: # 核心测试步骤:每个步骤对应一个接口请求
- id: device_bind # 步骤唯一标识,用于跨步骤取值
description: 新增设备
project: merchant # 所属项目(用于获取对应的host、token)
path: '/device/bind' # 接口路径
method: POST # 请求方法
headers:
Content-Type: 'application/json'
Authorization: '{{merchant.token}}' # 从全局变量取merchant的token
data: # 请求参数
code: deb45899-957-10972b35515
name: test_device_name
imei: '865403062000000'
assert: # 断言配置,支持多种断言类型
- type: equal # 等于断言
field: code # 响应字段:code
expected: 0 # 预期值
- type: is not None # 非空断言
field: data.id # 响应字段:data.id
- type: equal
field: message
expected: success
- id: device_list # 第二个步骤:查询新增的设备
description: 查询设备列表
project: merchant
path: '/device/list'
method: GET
headers:
Content-Type: 'application/json'
Authorization: '{{merchant.token}}'
data:
goodsId: '{{steps.device_bind.data.id}}' # 跨步骤取值:从device_bind步骤的响应中取id
assert:
- type: equal
field: status_code # 断言HTTP状态码
expected: 200
- type: equal
field: data.code
expected: '{{steps.device_bind.data.code}}' # 跨步骤取参数
- type: mysql_query # 数据库断言:查询设备是否存在
query: SELECT id FROM users WHERE name='test_device_name'
expected: id
teardowns: # 后置操作:测试完成后清理数据(如解绑设备、删除数据库记录)
- id: device_unbind
description: 解绑设备
operation_type: api # 操作类型:api=接口请求
project: plateform
path: '/device/unbind'
method: POST
headers:
Content-Type: 'application/json'
Authorization: '{{merchant.token}}'
data:
deviceId: '{{steps.device_bind.data.id}}' # 跨步骤取新增设备的id
assert:
- type: equal
field: code
expected: 0
- id: clear_database
description: 清理数据库
operation_type: db # 数据库操作
query: DELETE FROM device WHERE id = '{{steps.device_bind.data.id}}'
第二步:编写用例生成器(自动生成的 “核心引擎”)
用例生成器的作用是:读取 YAML 文件→校验数据格式→生成标准的 Pytest 用例代码,支持单个文件或目录批量处理。
以下是生成器核心代码(case_generator.py),关键逻辑已添加详细注释:
/* by yours.tools - online tools website : yours.tools/zh/iq.html */
# case_generator.py
# @author: xiaoqq
import os
import yaml
from utils.log_manager import log
class CaseGenerator:
"""
测试用例文件生成器
"""
def generate_test_cases(self, project_yaml_list=None, output_dir=None):
"""
根据YAML文件生成测试用例并保存到指定目录
:param project_yaml_list: 列表形式,项目名称或YAML文件路径
:param output_dir: 测试用例文件生成目录
"""
# 如果没有传入project_yaml_list,默认遍历tests目录下所有project
if not project_yaml_list:
project_yaml_list = ["tests/"]
# 遍历传入的project_yaml_list
for item in project_yaml_list:
if os.path.isdir(item): # 如果是项目目录,如tests/merchant
self._process_project_dir(item, output_dir)
elif os.path.isfile(item) and item.endswith('.yaml'): # 如果是单个YAML文件
self._process_single_yaml(item, output_dir)
else: # 如果是项目名称,如merchant
project_dir = os.path.join("tests", item)
self._process_project_dir(project_dir, output_dir)
log.info("测试用例生成完毕!")
def _process_project_dir(self, project_dir, output_dir):
"""
处理项目目录,遍历项目下所有YAML文件生成测试用例
:param project_dir: 项目目录路径
:param output_dir: 测试用例文件生成目录
"""
for root, dirs, files in os.walk(project_dir):
for file in files:
if file.endswith('.yaml'):
yaml_file = os.path.join(root, file)
self._process_single_yaml(yaml_file, output_dir)
def _process_single_yaml(self, yaml_file, output_dir):
"""
处理单个YAML文件,生成对应的测试用例文件
:param yaml_file: YAML文件路径
:param output_dir: 测试用例文件生成目录
"""
# 读取YAML文件内容
_test_data = self.load_test_data(yaml_file)
validate_test_data = self.validate_test_data(_test_data)
if not validate_test_data:
log.warning(f"{yaml_file} 数据校验不通过,跳过生成测试用例。")
return
test_data = _test_data['testcase']
teardowns = test_data.get('teardowns')
validate_teardowns = self.validate_teardowns(teardowns)
# 生成测试用例文件的相对路径。yaml文件路径有多个层级时,获取项目名称,以及tests/后、yaml文件名前的路径
relative_path = os.path.relpath(yaml_file, 'tests')
path_components = relative_path.split(os.sep)
project_name = path_components[0] if path_components[0] else path_components[1]
# 移除最后一个组件(文件名)
if path_components:
path_components.pop() # 移除最后一个元素
directory_path = os.path.join(*path_components) # 重新组合路径
directory_path = directory_path.rstrip(os.sep) # 确保路径不以斜杠结尾
module_name = test_data['name']
description = test_data.get('description')
# 日志记录中的测试用例名称
case_name = f"test_{module_name} ({description})" if description is not None else f"test_{module_name}"
# 判断test_data中的name是否存在"_",存在则去掉将首字母大写组成一个新的字符串,否则首字母大写
module_class_name = (''.join(s.capitalize() for s in module_name.split('_'))
if '_' in module_name else module_name.capitalize())
file_name = f'test_{module_name}.py'
# 生成文件路径
if output_dir:
file_path = os.path.join(output_dir, directory_path, file_name)
else:
file_path = os.path.join('test_cases', directory_path, file_name)
# 检查test_cases中对应的.py文件是否存在,存在则跳过生成
if os.path.exists(file_path):
log.info(f"测试用例文件已存在,跳过生成: {file_path}")
return
# 创建目录
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 解析Allure配置
allure_epic = test_data.get("allure", {}).get("epic", project_name)
allure_feature = test_data.get("allure", {}).get("feature")
allure_story = test_data.get("allure", {}).get("story", module_name)
# 生成并写入用例代码
with open(file_path, 'w', encoding='utf-8') as f:
# 写入导入语句
f.write(f"# Auto-generated test module for {module_name}\n")
f.write(f"from utils.log_manager import log\n")
f.write(f"from utils.globals import Globals\n")
f.write(f"from utils.variable_resolver import VariableResolver\n")
f.write(f"from utils.request_handler import RequestHandler\n")
f.write(f"from utils.assert_handler import AssertHandler\n")
if validate_teardowns:
f.write(f"from utils.teardown_handler import TeardownHandler\n")
f.write(f"from utils.project_login_handler import ProjectLoginHandler\n")
f.write(f"import allure\n")
f.write(f"import yaml\n\n")
# 写入类装饰器(Allure配置)
f.write(f"@allure.epic('{allure_epic}')\n")
if allure_feature:
f.write(f"@allure.feature('{allure_feature}')\n")
f.write(f"class Test{module_class_name}:\n")
# 写入setup_class(类级前置操作)
f.write(f" @classmethod\n")
f.write(f" def setup_class(cls):\n")
f.write(f" log.info('========== 开始执行测试用例:{case_name} ==========')\n")
f.write(f" cls.test_case_data = cls.load_test_case_data()\n") # 获取测试数据
# 如果存在teardowns,则将步骤列表转换为字典, 在下面的测试方法中通过 id 查找步骤的信息
if validate_teardowns:
f.write(f" cls.login_handler = ProjectLoginHandler()\n")
f.write(f" cls.teardowns_dict = {{teardown['id']: teardown for teardown in cls.test_case_data['teardowns']}}\n")
f.write(f" for teardown in cls.test_case_data.get('teardowns', []):\n")
f.write(f" project = teardown.get('project')\n")
f.write(f" if project:\n")
f.write(f" cls.login_handler.check_and_login_project(project, Globals.get('env'))\n")
# 将步骤列表转换为字典, 在下面的测试方法中通过 id 查找步骤的信息
f.write(f" cls.steps_dict = {{step['id']: step for step in cls.test_case_data['steps']}}\n")
f.write(f" cls.session_vars = {{}}\n")
f.write(f" cls.global_vars = Globals.get_data()\n") # 获取全局变量
# 创建VariableResolver实例并保存在类变量中
f.write(f" cls.VR = VariableResolver(global_vars=cls.global_vars, session_vars=cls.session_vars)\n")
f.write(f" log.info('Setup completed for Test{module_class_name}')\n\n")
# 写入加载测试数据的静态方法
f.write(f" @staticmethod\n")
f.write(f" def load_test_case_data():\n")
f.write(f" with open(r'{yaml_file}', 'r', encoding='utf-8') as file:\n")
f.write(f" test_case_data = yaml.safe_load(file)['testcase']\n")
f.write(f" return test_case_data\n\n")
# 写入核心测试方法
f.write(f" @allure.story('{allure_story}')\n")
f.write(f" def test_{module_name}(self):\n")
f.write(f" log.info('Starting test_{module_name}')\n")
# 遍历步骤,生成接口请求和断言代码
for step in test_data['steps']:
step_id = step['id']
step_project = step.get("project") # 场景测试用例可能会请求不同项目的接口,需要在每个step中指定对应的project
f.write(f" # Step: {step_id}\n")
f.write(f" log.info(f'开始执行 step: {step_id}')\n")
f.write(f" {step_id} = self.steps_dict.get('{step_id}')\n")
if step_project:
f.write(f" project_config = self.global_vars.get('{step_project}')\n")
else:
f.write(f" project_config = self.global_vars.get('{project_name}')\n")
# 生成请求代码
f.write(f" response = RequestHandler.send_request(\n")
f.write(f" method={step_id}['method'],\n")
f.write(f" url=project_config['host'] + self.VR.process_data({step_id}['path']),\n")
f.write(f" headers=self.VR.process_data({step_id}.get('headers')),\n")
f.write(f" data=self.VR.process_data({step_id}.get('data')),\n")
f.write(f" params=self.VR.process_data({step_id}.get('params')),\n")
f.write(f" files=self.VR.process_data({step_id}.get('files'))\n")
f.write(f" )\n")
f.write(f" log.info(f'{step_id} 响应:{{response}}')\n")
f.write(f" self.session_vars['{step_id}'] = response\n")
# 生成断言代码
if 'assert' in step:
f.write(f" db_config = project_config.get('mysql')\n")
f.write(f" AssertHandler().handle_assertion(\n")
f.write(f" asserts=self.VR.process_data({step_id}['assert']),\n")
f.write(f" response=response,\n")
f.write(f" db_config=db_config\n")
f.write(f" )\n\n")
# 写入teardown_class(类级后置操作)
if validate_teardowns:
f.write(f" @classmethod\n")
f.write(f" def teardown_class(cls):\n")
f.write(f" log.info('Starting teardown for the Test{module_class_name}')\n")
for teardown_step in teardowns:
teardown_step_id = teardown_step['id']
teardown_step_project = teardown_step.get("project")
f.write(f" {teardown_step_id} = cls.teardowns_dict.get('{teardown_step_id}')\n")
if teardown_step_project:
f.write(f" project_config = cls.global_vars.get('{teardown_step_project}')\n")
else:
f.write(f" project_config = cls.global_vars.get('{project_name}')\n")
# 处理API类型的后置操作
if teardown_step['operation_type'] == 'api':
f.write(f" response = RequestHandler.send_request(\n")
f.write(f" method={teardown_step_id}['method'],\n")
f.write(f" url=project_config['host'] + cls.VR.process_data({teardown_step_id}['path']),\n")
f.write(f" headers=cls.VR.process_data({teardown_step_id}.get('headers')),\n")
f.write(f" data=cls.VR.process_data({teardown_step_id}.get('data')),\n")
f.write(f" params=cls.VR.process_data({teardown_step_id}.get('params')),\n")
f.write(f" files=cls.VR.process_data({teardown_step_id}.get('files'))\n")
f.write(f" )\n")
f.write(f" log.info(f'{teardown_step_id} 响应:{{response}}')\n")
f.write(f" cls.session_vars['{teardown_step_id}'] = response\n")
if 'assert' in teardown_step:
# if any(assertion['type'].startswith('mysql') for assertion in teardown_step['assert']):
# f.write(f" db_config = project_config.get('mysql')\n")
f.write(f" db_config = project_config.get('mysql')\n")
f.write(f" AssertHandler().handle_assertion(\n")
f.write(f" asserts=cls.VR.process_data({teardown_step_id}['assert']),\n")
f.write(f" response=response,\n")
f.write(f" db_config=db_config\n")
f.write(f" )\n\n")
# 处理数据库类型的后置操作
elif teardown_step['operation_type'] == 'db':
f.write(f" db_config = project_config.get('mysql')\n")
f.write(f" TeardownHandler().handle_teardown(\n")
f.write(f" asserts=cls.VR.process_data({teardown_step_id}),\n")
f.write(f" db_config=db_config\n")
f.write(f" )\n\n")
f.write(f" pass\n")
else:
log.info(f"未知的 operation_type: {teardown_step['operation_type']}")
f.write(f" pass\n")
f.write(f" log.info('Teardown completed for Test{module_class_name}.')\n")
f.write(f"\n log.info(f\"Test case test_{module_name} completed.\")\n")
log.info(f"已生成测试用例文件: {file_path}")
@staticmethod
def load_test_data(test_data_file):
"""读取YAML文件,处理读取异常"""
try:
with open(test_data_file, 'r', encoding='utf-8') as file:
test_data = yaml.safe_load(file)
return test_data
except FileNotFoundError:
log.error(f"未找到测试数据文件: {test_data_file}")
except yaml.YAMLError as e:
log.error(f"YAML配置文件解析错误: {e},{test_data_file} 跳过生成测试用例。")
@staticmethod
def validate_test_data(test_data):
"""校验测试数据格式是否符合要求"""
if not test_data:
log.error("test_data 不能为空.")
return False
if not test_data.get('testcase'):
log.error("test_data 必须包含 'testcase' 键.")
return False
if not test_data['testcase'].get('name'):
log.error("'testcase' 下的 'name' 字段不能为空.")
return False
steps = test_data['testcase'].get('steps')
if not steps:
log.error("'testcase' 下的 'steps' 字段不能为空.")
return False
for step in steps:
if not all(key in step for key in ['id', 'path', 'method']):
log.error("每个步骤必须包含 'id', 'path', 和 'method' 字段.")
return False
if not step['id']:
log.error("步骤中的 'id' 字段不能为空.")
return False
if not step['path']:
log.error("步骤中的 'path' 字段不能为空.")
return False
if not step['method']:
log.error("步骤中的 'method' 字段不能为空.")
return False
return True
@staticmethod
def validate_teardowns(teardowns):
"""
验证 teardowns 数据是否符合要求
:param teardowns: teardowns 列表
:return: True 如果验证成功,否则 False
"""
if not teardowns:
# log.warning("testcase 下的 'teardowns' 字段为空.")
return False
for teardown in teardowns:
if not all(key in teardown for key in ['id', 'operation_type']):
log.warning("teardown 必须包含 'id' 和 'operation_type' 字段.")
return False
if not teardown['id']:
log.warning("teardown 中的 'id' 字段为空.")
return False
if not teardown['operation_type']:
log.warning("teardown 中的 'operation_type' 字段为空.")
return False
if teardown['operation_type'] == 'api':
required_api_keys = ['path', 'method', 'headers', 'data']
if not all(key in teardown for key in required_api_keys):
log.warning("对于 API 类型的 teardown,必须包含 'path', 'method', 'headers', 'data' 字段.")
return False
if not teardown['path']:
log.warning("teardown 中的 'path' 字段为空.")
return False
if not teardown['method']:
log.warning("teardown 中的 'method' 字段为空.")
return False
elif teardown['operation_type'] == 'db':
if 'query' not in teardown or not teardown['query']:
log.warning("对于数据库类型的 teardown,'query' 字段不能为空.")
return False
return True
if __name__ == '__main__':
# 运行生成器,生成指定YAML文件的用例
CG = CaseGenerator()
CG.generate_test_cases(project_yaml_list=["tests/merchant/test_device_bind.yaml"])
第三步:运行生成器,自动生成Pytest用例
运行上述生成器代码后,会自动在指定目录(默认test_cases)生成标准化的Pytest用例文件(如test_device_bind.py),无需手动修改,可通过项目入口文件执行(入口文件详细代码可参考文末开源项目)。
生成的用例代码示例(关键部分):
# Auto-generated test module for device_bind
from utils.log_manager import log
from utils.globals import Globals
from utils.variable_resolver import VariableResolver
from utils.request_handler import RequestHandler
from utils.assert_handler import AssertHandler
from utils.teardown_handler import TeardownHandler
import allure
import yaml
@allure.epic('商家端')
@allure.feature('设备管理')
class TestDeviceBind:
@classmethod
def setup_class(cls):
log.info('========== 开始执行测试用例:test_device_bind (新增设备) ==========')
cls.test_case_data = cls.load_test_case_data()
cls.steps_dict = {step['id']: step for step in cls.test_case_data['steps']}
cls.session_vars = {}
cls.global_vars = Globals.get_data()
cls.VR = VariableResolver(global_vars=cls.global_vars, session_vars=cls.session_vars)
log.info('Setup 完成')
@staticmethod
def load_test_case_data():
with open(r'tests/merchant\device_management\test_device_bind.yaml', 'r', encoding='utf-8') as file:
test_case_data = yaml.safe_load(file)['testcase']
return test_case_data
@allure.story('新增设备')
def test_device_bind(self):
log.info('开始执行 test_device_bind')
# Step: device_bind
log.info(f'开始执行 step: device_bind')
device_bind = self.steps_dict.get('device_bind')
project_config = self.global_vars.get('merchant')
response = RequestHandler.send_request(
method=spu_deviceType['method'],
url=project_config['host'] + self.VR.process_data(device_bind['path']),
headers=self.VR.process_data(device_bind.get('headers')),
data=self.VR.process_data(device_bind.get('data')),
params=self.VR.process_data(device_bind.get('params')),
files=self.VR.process_data(device_bind.get('files'))
)
log.info(f'device_bind 请求结果为:{response}')
self.session_vars['device_bind'] = response
db_config = project_config.get('mysql')
AssertHandler().handle_assertion(
asserts=self.VR.process_data(device_bind['assert']),
response=response,
db_config=db_config
)
# Step: device_list
log.info(f'开始执行 step: device_list')
device_list = self.steps_dict.get('device_list')
project_config = self.global_vars.get('merchant')
response = RequestHandler.send_request(
method=device_list['method'],
url=project_config['host'] + self.VR.process_data(device_list['path']),
headers=self.VR.process_data(device_list.get('headers')),
data=self.VR.process_data(device_list.get('data')),
params=self.VR.process_data(device_list.get('params')),
files=self.VR.process_data(device_list.get('files'))
)
log.info(f'device_list 请求结果为:{response}')
self.session_vars['device_list'] = response
db_config = project_config.get('mysql')
AssertHandler().handle_assertion(
asserts=self.VR.process_data(device_list['assert']),
response=response,
db_config=db_config
)
log.info(f"Test case test_device_bind completed.")
@classmethod
def teardown_class(cls):
# 示例代码省略
......
log.info(f'Teardown completed for TestDeviceBind.')
四、其他核心工具类
生成的用例文件依赖多个自定义工具类,这些工具类封装了通用功能,确保用例可正常运行。以下是各工具类的核心作用(详细实现可参考文末开源项目):
| 工具类 | 作用 |
|---|---|
| log_manager | 统一日志记录,输出用例执行过程 |
| Globals | 存储全局配置,如各项目的host、token、数据库连接信息、环境变量等。 |
| VariableResolver | 解析 YAML 中的变量(如{{steps.device_bind.data.id}}),支持全局变量、跨步骤变量取值。 |
| RequestHandler | 统一发送 HTTP 请求,处理超时、重试 |
| AssertHandler | 解析YAML中的断言配置,支持常规断言(等于、非空、包含等)和数据库断言。 |
| TeardownHandler | 处理后置操作,支持接口请求型和数据库操作型的后置清理逻辑。 |
五、方案落地价值:重构后我们获得了什么?
- 效率翻倍:用例编写时间减少 70%+。以前写一条 3 步流程用例要 15 分钟,现在写 YAML 只需要 5 分钟,生成用例秒级完成,还不用关心代码格式。
- 维护成本大幅降低:接口变更时,仅需修改对应YAML文件的相关字段(如参数、断言),重新运行生成器即可更新用例,无需全局搜索和修改代码,避免引入新bug。
- 入门门槛极低:无Python基础的测试人员,只需学习简单的YAML格式规则,按模板填写数据即可参与用例编写,团队协作效率大幅提升。
- 项目规范统一:所有用例的命名、目录结构、日志格式、断言方式均由生成器统一控制,彻底告别“各自为战”的混乱局面,项目可维护性显著增强。
六、后续优化方向
目前方案已满足核心业务需求,但仍有优化空间,后续将重点推进以下方向:
- 支持用例间依赖:实现用例级别的数据传递,比如用例A的输出作为用例B的输入,满足更复杂的业务场景。
- 增强YAML灵活性:支持在YAML中调用自定义Python函数(如生成随机数、加密参数),提升数据设计的灵活性。
- 简化YAML编写:增加通用配置默认值(如默认请求头、默认项目配置),减少重复填写工作。
- 多数据源支持:新增Excel/CSV导入功能,满足不熟悉YAML格式的测试人员需求,进一步降低使用门槛。
七、参考项目
如果想直接落地,可以参考我的开源示例项目:api-auto-test,里面包含了完整的工具类实现、YAML 模板、生成器代码和执行脚本。
964

被折叠的 条评论
为什么被折叠?



