响应式网站制作公司网站开发工具与环境
2025/12/26 5:46:01 网站建设 项目流程
响应式网站制作公司,网站开发工具与环境,做财经类网站要许可吗,视频网站系统开发引言#xff1a;为什么我们要抛弃 “手写用例”#xff1f; 在接口自动化实践中#xff0c;当项目规模扩大、用例数量激增时#xff0c;传统“手写Pytest用例”的模式往往会陷入瓶颈。 做接口自动化的同学#xff0c;大概率都踩过这样的硬编码坑#xff1a;写一条 “新…引言为什么我们要抛弃 “手写用例”在接口自动化实践中当项目规模扩大、用例数量激增时传统“手写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 nametest_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_listNone, output_dirNone): 根据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 ftest_{module_name} ({description}) if description is not None else ftest_{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 ftest_{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_okTrue) # 解析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, encodingutf-8) as f: # 写入导入语句 f.write(f# Auto-generated test module for {module_name}\n) f.write(ffrom utils.log_manager import log\n) f.write(ffrom utils.globals import Globals\n) f.write(ffrom utils.variable_resolver import VariableResolver\n) f.write(ffrom utils.request_handler import RequestHandler\n) f.write(ffrom utils.assert_handler import AssertHandler\n) if validate_teardowns: f.write(ffrom utils.teardown_handler import TeardownHandler\n) f.write(ffrom utils.project_login_handler import ProjectLoginHandler\n) f.write(fimport allure\n) f.write(fimport yaml\n\n) # 写入类装饰器Allure配置 f.write(fallure.epic({allure_epic})\n) if allure_feature: f.write(fallure.feature({allure_feature})\n) f.write(fclass 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_varscls.global_vars, session_varscls.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, encodingutf-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 urlproject_config[host] self.VR.process_data({step_id}[path]),\n) f.write(f headersself.VR.process_data({step_id}.get(headers)),\n) f.write(f dataself.VR.process_data({step_id}.get(data)),\n) f.write(f paramsself.VR.process_data({step_id}.get(params)),\n) f.write(f filesself.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 assertsself.VR.process_data({step_id}[assert]),\n) f.write(f responseresponse,\n) f.write(f db_configdb_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 urlproject_config[host] cls.VR.process_data({teardown_step_id}[path]),\n) f.write(f headerscls.VR.process_data({teardown_step_id}.get(headers)),\n) f.write(f datacls.VR.process_data({teardown_step_id}.get(data)),\n) f.write(f paramscls.VR.process_data({teardown_step_id}.get(params)),\n) f.write(f filescls.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 assertscls.VR.process_data({teardown_step_id}[assert]),\n) f.write(f responseresponse,\n) f.write(f db_configdb_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 assertscls.VR.process_data({teardown_step_id}),\n) f.write(f db_configdb_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, encodingutf-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(fYAML配置文件解析错误: {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(对于数据库类型的 teardownquery 字段不能为空.) 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_varscls.global_vars, session_varscls.session_vars) log.info(Setup 完成) staticmethod def load_test_case_data(): with open(rtests/merchant\device_management\test_device_bind.yaml, r, encodingutf-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( methodspu_deviceType[method], urlproject_config[host] self.VR.process_data(device_bind[path]), headersself.VR.process_data(device_bind.get(headers)), dataself.VR.process_data(device_bind.get(data)), paramsself.VR.process_data(device_bind.get(params)), filesself.VR.process_data(device_bind.get(files)) ) log.info(fdevice_bind 请求结果为{response}) self.session_vars[device_bind] response db_config project_config.get(mysql) AssertHandler().handle_assertion( assertsself.VR.process_data(device_bind[assert]), responseresponse, db_configdb_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( methoddevice_list[method], urlproject_config[host] self.VR.process_data(device_list[path]), headersself.VR.process_data(device_list.get(headers)), dataself.VR.process_data(device_list.get(data)), paramsself.VR.process_data(device_list.get(params)), filesself.VR.process_data(device_list.get(files)) ) log.info(fdevice_list 请求结果为{response}) self.session_vars[device_list] response db_config project_config.get(mysql) AssertHandler().handle_assertion( assertsself.VR.process_data(device_list[assert]), responseresponse, db_configdb_config ) log.info(fTest case test_device_bind completed.) classmethod def teardown_class(cls): # 示例代码省略 ...... log.info(fTeardown 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 模板、生成器代码和执行脚本。左边二维码为博主个人微信扫码添加微信后可加入测试学习交流群添加时请务必备注加入测试学习交流群。右边二维码为博主微信公众号专注于自动化测试、测试开发技术分享欢迎关注。 书山有路勤为径学海无涯苦做舟。希望通过分享学习交流大家能够朝着最朴实的愿望--成长、加薪、升职更进一步。本文作者给你一页白纸版权申明本博客所有文章除特殊声明外均采用 BY-NC-SA 许可协议。转载请注明出处声援博主如果觉得这篇文章对您有帮助请点一下右下角的 “推荐” 图标哦您的 “推荐” 是我写作的最大动力。您也可以点击下方的 【关注我】 按钮关注博主不迷路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询