diff --git a/README.md b/README.md index cf1dcfa..2b47acf 100644 --- a/README.md +++ b/README.md @@ -43,70 +43,7 @@ -## 三、目录结构 -``` -├────case_utils/ 测试框架相关工具类 -│ ├────__init__.py -│ ├────allure_handle.py 操作allure的相关方法 -│ ├────assert_handle.py 断言处理, 包括响应断言和数据库断言 -│ ├────case_data_analysis 分析用例数据是否符合规范 -│ ├────case_fun_handle.py 根据配置文件,从指定类型文件中读取用例数据,并调用生成用例文件方法,生成用例文件 -│ ├────data_handle.py 数据处理 -│ ├────extract_data_handle.py 提取数据的一些方法 -│ ├────get_results_handle.py 从allure测试报告中获取测试结果 -│ ├────platform_handle.py 跨平台的支持allure,用于生成allure测试报告 -│ ├────request_data_handle.py 针对用例数据进行请求前后的处理 -│ └────send_result_handle.py 根据配置文件,发送指定类型的测试结果 -├────common_utils/ 公共的工具类 -│ ├────__init__.py -│ ├────base_request.py 封装的requests请求 -│ ├────dingding_handle.py 封装的钉钉机器人 -│ ├────excel_handle.py 处理excel -│ ├────files_handle.py 处理文件相关操作 -│ ├────func_handle.py 函数装饰器 -│ ├────http_server.py 封装的HTTP服务 -│ ├────mysql_handle.py 使用pymysql模块连接mysql数据库的公共方法 -│ ├────time_handle.py 封装处理时间操作的一些方法 -│ ├────wechat_handle.py 封装企业微信机器人 -│ ├────yagmail_handle.py 封装通过yagmail发送邮件的方法 -│ └────yaml_handle.py 处理yaml文件 -├────config/ -│ ├────__init__.py -│ ├────allure_config/ -│ │ ├────gitlinklogo.jpg 保存用来替换allure报告的logo的,在代码中无用处 -│ │ ├────http_server.exe http服务,用来放置在allure压缩包中,方便在不安装allure环境下打开allure报告 -│ │ ├────logo.svg 保存用来替换allure报告的logo的,在代码中无用处 -│ │ ├────双击打开Allure报告.bat .bat文件,用来放置在allure压缩包中,方便在不安装allure环境下打开allure报告 -│ ├────case_template.txt 自动生成的测试用例文件模板 -│ ├────global_vars.py 保存的一些全局变量 -│ ├────path_config.py 项目路径管理 -│ └────settings.py 配置文件 -├────conftest.py -├────data/ 测试用例数据 -│ ├────test_login_demo.yaml -│ ├────test_login_excel_demo.xlsx -│ └────test_new_project_demo.yaml -├────outputs/ -│ └────report/ 保存测试报告的目录 -│ └────log/ 保存日志文件的目录 -├────files 存放测试过程中需要上传的文件 -├────Pipfile -├────pytest.ini -├────README.md -├────run.py 运行入口 -└────test_case/ 测试用例 -│ ├────conftest.py -│ ├────test_auto_case/ 自动生成的测试用例目录 -│ │ ├────test_login_demo.py -│ │ ├────test_login_excel_demo.py -│ │ └────test_new_project_demo.py -│ └────test_manual_case/ 手动编写的测试用例目录 -│ │ ├────__init__.py -│ │ ├────test_demo.py -│ │ └────test_login_demo.py - ``` - -## 四、依赖库 +## 三、依赖库 ``` pymysql = "*" loguru = "*" @@ -128,23 +65,23 @@ xpinyin = "*" ``` -## 五、安装教程 +## 四、安装教程 1. 通过Git工具clone代码到本地 或者 直接下载压缩包ZIP ``` -git clone https://gitlink.org.cn/floraachy/uiautotest.git +https://gitlink.org.cn/floraachy/apiautotest.git ``` -3. 本地电脑搭建好 python环境,我使用的python版本是3.9 +2. 本地电脑搭建好 python环境,我使用的python版本是3.9。包括allure测试报告所需的java环境(安装jdk)。 -4. 安装pipenv +3. 安装pipenv ``` # 建议在项目根目录下执行命令安装 pip install pipenv ``` -6. 使用pipenv管理安装环境依赖包:pipenv install (必须在项目根目录下执行) +4. 使用pipenv管理安装环境依赖包:pipenv install (必须在项目根目录下执行) ``` 注意:使用pipenv install会自动安装Pipfile里面的依赖包,该依赖包仅安装在虚拟环境里,不安装在测试机。 ``` @@ -158,7 +95,7 @@ pip install pipenv - 另外,我们如果是使用pycharm直接右键run的情况,我们需要pycharm解释器选择我们创建的虚拟环境,才不会报少包的错误。 -## 六、如何创建用例 +## 五、如何创建用例 注意:如果想用我框架中的用例执行运行测试,可以暂时跳过这一章节,直接看章节 "运行自动化测试"。 @@ -168,21 +105,21 @@ pip install pipenv 3)确认测试是否需要进行数据库断言,如有需求,填充数据库配置信息 4)指定日志收集级别,由LOG_LEVEL控制 -### 2. 修改全局变量,增加测试数据 `config.global_vars.py` +### 2. 修改全局变量,增加测试数据 `config.settings.py` 1) ENV_VARS["common"]是一些公共参数,如报告标题,报告名称,测试者,测试部门。后续会显示在测试报告上。如果还有其他,可自行添加 2)ENV_VARS["test"]是保存test环境的一些测试数据。ENV_VARS["live"]是保存live环境的一些测试数据。如果还有其他环境可以继续增加,例如增加ENV_VARS["dev"] = {"host": "", ......} ### 3. 删除框架中的示例用例数据 1)删除 `data`目录下所有的YAML和EXCEL文件 -2)删除 `test_case/test_manual_case`目录下所有手动编写的用例 +2)删除 `test_case/test_manual_case`目录下所有手动编写的用例,后续有需要可以在该目录下手动编写用例。 ### 4. 编写测试用例(两种方式任选其一或者都选) -#### 1 自动生成测试用例 `data` `test_case.test_auto_case` +#### 1)自动生成测试用例 `data` `test_case.test_auto_case` - 在目录`data`下新建一个YAML/Excel文件。按照如下字段要求进行测试用例数据添加 - 注意:如果需要自动创建测试用例文件,YAML/Excel文件的文件名需要以"test"开头。 -#### 2. 手动编写测试用例 `data` `test_case.test_manual_case` +#### 2)手动编写测试用例 `data` `test_case.test_manual_case` - 原则上,如果是手动编写测试用例(python代码), 测试用例数据文件不要以"test"开头。 如果以“test”开头,可能导致用例运行多次。 1)在目录`data`下新建一个YAML/Excel文件,按照要求编写测试用例数据 2)在test_case.test_manual_case下新建一个以"test"开头的测试方法,进行测试用例方法编写。 @@ -206,7 +143,7 @@ case_info: 具体的用例数据,是以列表的形式进行管理 cookies:请求cookies,格式是:DICT, CookieJar对象 request_type:请求数据类型:params, json, file, data payload:请求参数 - files: 需要上传的文件,参考如下传参:{接口中文件参数的名称:'文件路径地址'/['文件地址1', '文件地址2']} + files: 需要上传的文件,参考如下传参:{接口中文件参数的名称:'文件路径地址'} extract:后置提取参数 assert_response:响应断言 assert_sql:数据库断言 @@ -237,22 +174,23 @@ excel表单2名称是:示例模块 测试用例类:TestDemoSlmkAuto 测试用例方法:test_demo_slmk_auto -## 七、运行自动化测试 +## 六、运行自动化测试 ### 1. 激活已存在的虚拟环境 - (如果不存在会创建一个):pipenv shell (必须在项目根目录下执行) ### 2. 运行 ``` 在pycharm>terminal或者电脑命令窗口,进入项目根路径,执行如下命令(如果依赖包是安装在虚拟环境中,需要先启动虚拟环境)。 - > python run.py (默认在test环境运行测试用例, 报告采用allure) + > python run.py 默认在test环境运行测试用例, 生成allure测试报告 + > python run.py -m demo 在test环境仅运行打了标记demo用例,生成allure测试报告 > python run.py -env live 在live环境运行测试用例 > python run.py -env=test 在test环境运行测试用例 - > python run.py -report=pytest-html (默认在test环境运行测试用例, 报告采用pytest-html) + > python run.py -report=no 在test环境下允许测试用例,不生成allure测试报告 ``` 注意: - 如果pycharm.interpreter拥有了框架所需的所有依赖包,可以通过pycharm直接在`run.py`中右键运行 -## 八、查看测试报告 +## 七、查看测试报告 ### Allure测试报告 1. Allure生成的测试报告,支持通过pycharm,点击`outputs/report/allure_html/index.html`打开查看测试报告 2. 如果不通过pycharm打开,直接通过文件夹打开,windows系统环境下,可以点击`outputs/report/allure_html/双击打开Allure报告.bat`打开查看测试报告 @@ -264,7 +202,7 @@ excel表单2名称是:示例模块 - 如果通过点击`outputs/report/allure_html/双击打开Allure报告.bat`打开测试报告,命令窗口显示乱码,或者打不开,可以把`.bat`的文件名称修改为英文的名称,里面的所有中文注释全部移除,再次尝试 -## 九 、详细功能说明 +## 八 、详细功能说明 - [如何实现动态数据、随机数据的热加载?](https://www.gitlink.org.cn/zone/tester/newdetail/236) 我们有些特殊的场景,可能会涉及到一些定制化的数据,每次执行数据,需要按照指定规则随机生成,实时加载数据,那么这部分应该如何处理呢? @@ -307,7 +245,7 @@ excel表单2名称是:示例模块 -## 十、初始化项目可能遇到的问题 +## 九、初始化项目可能遇到的问题 - [测试机安装的是python3.7,但是本框架要求3.9.5,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/245) - [无法安装依赖包或者安装很慢,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/244) diff --git a/case_utils/assert_utils/__init__.py b/case_utils/assert_utils/__init__.py new file mode 100644 index 0000000..5ec09e5 --- /dev/null +++ b/case_utils/assert_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:40 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/case_utils/assert_handle.py b/case_utils/assert_utils/assert_handle.py similarity index 82% rename from case_utils/assert_handle.py rename to case_utils/assert_utils/assert_handle.py index 56ae11b..6fa7e79 100644 --- a/case_utils/assert_handle.py +++ b/case_utils/assert_utils/assert_handle.py @@ -11,13 +11,13 @@ from loguru import logger from requests import Response import allure # 本地应用/模块导入 -from case_utils.extract_data_handle import json_extractor, re_extract -from case_utils.request_data_handle import response_type -from case_utils.allure_handle import custom_allure_step -from common_utils.mysql_handle import MysqlServer +from case_utils.requests_utils.extract_data_handle import json_extractor, re_extract +from case_utils.requests_utils.request_data_handle import response_type +from case_utils.report_utils.allure_handle import allure_step +from common_utils.database_utils.mysql_handle import MysqlServer -@allure.step("响应断言") +@allure.step("响应断言 --> 响应数据:{response} - 预期结果:{expected}") def assert_response(response: Response, expected: dict) -> None: """ 断言方法 :param response: 实际响应对象 @@ -31,12 +31,10 @@ def assert_response(response: Response, expected: dict) -> None: return None """ logger.info("\n======================================================\n" \ - f"-------------Start:响应断言--------------------\n") + f"-------------Start:响应断言--------------------\n" + f"响应断言预期结果:{expected}, {type(expected)}") if expected is None: - logger.info("判断是否存在响应断言---->当前用例无响应断言!") - custom_allure_step(step_title='判断是否存在响应断言---->当前用例无响应断言!') return - logger.debug(f"响应断言预期结果:{expected}, {type(expected)}") index = 0 for k, v in expected.items(): # 获取需要断言的实际结果部分 @@ -52,7 +50,7 @@ def assert_response(response: Response, expected: dict) -> None: actual = re_extract(response.text, _k) index += 1 logger.info(f'第{index}个响应断言 -|- 预期结果: {_k}: {_v}, {type(_v)} {k} 实际结果: {actual}, {type(actual)}') - custom_allure_step( + allure_step( step_title=f'第{index}个响应断言数据---->预期结果: {_k}: {_v}, {type(_v)} {k} 实际结果: {actual}, {type(actual)}') try: if k == "eq": # 预期结果 = 实际结果 @@ -72,11 +70,11 @@ def assert_response(response: Response, expected: dict) -> None: logger.success(f"预期结果: {_k}: {_v} != 实际结果: {actual}, 断言成功!") else: logger.error(f"判断关键字: {k} 错误!, 目前仅支持如下关键字:eq, in, gt, lt, not") - custom_allure_step(step_title=f'判断关键字: {k} 错误!', + allure_step(step_title=f'判断关键字: {k} 错误!', content='目前仅支持如下关键字:eq, in, gt, lt, not') except AssertionError: logger.error(f"第{index}个响应断言失败 -|- 预期结果: {_k}: {_v}, {type(_v)} {k} 实际结果: {actual}, {type(actual)}") - custom_allure_step( + allure_step( step_title=f'第{index}个响应断言失败---->预期结果: {_k}: {_v}, {type(_v)} {k} 实际结果: {actual}, {type(actual)}') logger.info('\n-------------End:响应断言--------------------\n' \ "=====================================================") @@ -104,11 +102,11 @@ def assert_sql(db_info, expected: dict): f"-------------Start:数据库断言--------------------") if expected is None: logger.info("判断是否存在数据库断言---->当前用例无数据库断言") - custom_allure_step(step_title='判断是否存在数据库断言---->当前用例无数据库断言') + allure_step(step_title='判断是否存在数据库断言---->当前用例无数据库断言') return if not db_info: logger.error("判断是否存在数据库配置---->当前环境无数据库配置,跳过数据库断言!") - custom_allure_step(step_title='判断是否存在数据库配置---->当前环境无数据库配置,跳过数据库断言!') + allure_step(step_title='判断是否存在数据库配置---->当前环境无数据库配置,跳过数据库断言!') logger.info('\n-------------End:数据库断言--------------------\n' "=====================================================") return @@ -120,11 +118,11 @@ def assert_sql(db_info, expected: dict): # 查询数据库,获取查询结果 sql_result = MysqlServer(**db_info).query_one(_v) logger.info(f'数据库响应断言 -|- SQL:{_v} || 查询结果:{sql_result}') - custom_allure_step(step_title=f'数据库断言---->SQL:{_v} ', + allure_step(step_title=f'数据库断言---->SQL:{_v} ', content=f'查询结果:{sql_result}') except Exception as e: logger.error(f'数据库服务报错:{e}') - custom_allure_step(step_title=f'数据库服务报错', + allure_step(step_title=f'数据库服务报错', content=f'{e} ') logger.info('\n-------------End:数据库断言--------------------\n' "=====================================================") @@ -135,16 +133,16 @@ def assert_sql(db_info, expected: dict): if _k == "len": assert _v == len(sql_result) logger.success(f"预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!") - custom_allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!') + allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!') # 如果时$.开头,则从数据库查询结果中提取相应的值作为实际结果 elif _k.startswith("$."): actual = json_extractor(sql_result, _k) assert _v == actual logger.success(f"预期结果: {_v} == 实际结果: {actual}, 断言成功!") - custom_allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {actual}, 断言成功!') + allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {actual}, 断言成功!') except AssertionError: logger.error(f"数据库断言失败 -|- 预期结果:{_v} {k} 实际结果: {sql_result})") - custom_allure_step(step_title=f'数据库断言失败---->预期结果:{_v} {k} 实际结果: {sql_result}') + allure_step(step_title=f'数据库断言失败---->预期结果:{_v} {k} 实际结果: {sql_result}') logger.info('\n-------------End:数据库断言--------------------\n' \ "=====================================================") raise AssertionError(f"数据库断言失败 -|-预期结果: {_v} {k} 实际结果: {sql_result}") diff --git a/case_utils/logger_utils/__init__.py b/case_utils/logger_utils/__init__.py new file mode 100644 index 0000000..5e4fb07 --- /dev/null +++ b/case_utils/logger_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:42 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/case_utils/logger_utils/log_handle.py b/case_utils/logger_utils/log_handle.py new file mode 100644 index 0000000..55cd40a --- /dev/null +++ b/case_utils/logger_utils/log_handle.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# @Version: Python 3.9 +# @Time : 2023/1/9 17:09 +# @Author : chenyinhua +# @File : log_handle.py +# @Software: PyCharm +# @Desc: 日志处理 + +# 标准库导入 +import sys +# 第三方库导入 +from loguru import logger + + +def capture_logs(filename, level="TRACE", filter_type=None): + """ + 日志处理 + 文档参考:https://zhuanlan.zhihu.com/p/429452898 + 基本参数释义: + sink:可以是一个 file 对象,例如 sys.stderr 或 open('file.log', 'w'),也可以是 str 字符串或者 pathlib.Path 对象,即文件路径,也可以是一个方法,可以自行定义输出实现,也可以是一个 logging 模块的 Handler,比如 FileHandler、StreamHandler 等,还可以是 coroutine function,即一个返回协程对象的函数等。 + level:日志输出和保存级别。 + format:日志格式模板。 + filter:一个可选的指令,用于决定每个记录的消息是否应该发送到 sink。 + colorize:格式化消息中包含的颜色标记是否应转换为用于终端着色的 ansi 代码,或以其他方式剥离。 如果没有,则根据 sink 是否为 tty(电传打字机缩写) 自动做出选择。 + serialize:在发送到 sink 之前,是否应首先将记录的消息转换为 JSON 字符串。 + backtrace:格式化的异常跟踪是否应该向上扩展,超出捕获点,以显示生成错误的完整堆栈跟踪。 + diagnose:异常跟踪是否应显示变量值以简化调试。建议在生产环境中设置 False,避免泄露敏感数据。 + enqueue:要记录的消息是否应在到达 sink 之前首先通过多进程安全队列,这在通过多个进程记录到文件时很有用,这样做的好处还在于使日志记录调用是非阻塞的。 + catch:是否应自动捕获 sink 处理日志消息时发生的错误,如果为 True,则会在 sys.stderr 上显示异常消息,但该异常不会传播到 sink,从而防止应用程序崩溃。 + \kwargs:仅对配置协程或文件接收器有效的附加参数 + + 日志级别,从低到高: + logger.trace() 等级5 + logger.debug() 等级10 + logger.info() 等级20 + logger.success() 等级25 + logger.warning() 等级30 + logger.error() 等级40 + logger.critical() 等级50 + :param filename: 日志文件名 + :param filter_type: 日志过滤,如:将日志级别为ERROR的单独记录到一个文件中 + :param level: 日志级别设置 + """ + logger.remove() # 清除之前的设置 + dic = dict(sink=filename, # 日志保存路径 + rotation='10 MB', + retention='3 days', + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | From {module}.{function}.{line} : {message}", # 日志输出格式 + encoding='utf-8', + level=level, # 日志级别设置 + enqueue=True + ) + if filter_type: + dic["filter"] = lambda x: filter_type in str(x['level']).upper() + + logger.add(**dic) + # 添加控制台输出 + logger.add(sink=sys.stderr, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | From {module}.{function}.{line} : {message}") diff --git a/case_utils/report_utils/__init__.py b/case_utils/report_utils/__init__.py new file mode 100644 index 0000000..1d36ec7 --- /dev/null +++ b/case_utils/report_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:39 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/case_utils/allure_handle.py b/case_utils/report_utils/allure_handle.py similarity index 69% rename from case_utils/allure_handle.py rename to case_utils/report_utils/allure_handle.py index a892bbe..7865f30 100644 --- a/case_utils/allure_handle.py +++ b/case_utils/report_utils/allure_handle.py @@ -6,12 +6,14 @@ # @Desc: # 标准库导入 -import json import os +import json # 第三方库导入 import allure # 本地应用/模块导入 from config.models import AllureAttachmentType +from case_utils.report_utils.platform_handle import PlatformHandle +from common_utils.other_utils.files_handle import zip_file, copy_file def allure_title(title: str) -> None: @@ -20,7 +22,7 @@ def allure_title(title: str) -> None: allure.dynamic.title(title) -def custom_allure_step(step_title: str, content: str = None, source=None) -> None: +def allure_step(step_title: str, content: str = None, source=None) -> None: """ allure.step()添加测试用例步骤 :param step_title: 步骤及附件名称 @@ -72,8 +74,12 @@ class AllureReportBeautiful: @param allure_results_path: allure保存测试结果集目录 @param allure_html_path: allure生成的html报告的目录 """ - self.allure_html_path = allure_html_path - self.allure_results_path = allure_results_path + if os.path.exists(allure_html_path) and os.path.exists(allure_results_path): + self.allure_html_path = allure_html_path + self.allure_results_path = allure_results_path + else: + print(f"allure地址错误!allure_results_path={allure_results_path}, allure_html_path={allure_html_path}") + raise "allure地址错误!" # 设置报告窗口的标题 def set_windows_title(self, new_title): @@ -162,3 +168,45 @@ def allure_logo_change(allure_path, logo_path): """ # TODO 后续支持通过代码实现修改 pass + + +def generate_allure_report(**kwargs): + """ + 通过allure生成html测试报告,并对报告进行美化 + """ + allure_results_dir = kwargs.get("allure_results") + allure_report_dir = kwargs.get("allure_report") + # ----------------判断运行的平台,是linux还是windows,执行不同的allure命令---------------- + cmd = f"{PlatformHandle().allure} generate {allure_results_dir} -o {allure_report_dir} --clean" + # 如果html报告没有生成,请检查下是否正确安装jdk(最好默认安装,不要自定义路径);安装完成后,要注意重启pycharm + os.popen(cmd).read() + # ----------------美化allure测试报告 ------------------------------------------ + # 设置打开的 Allure 报告的浏览器窗口标题文案 + allure_beautiful = AllureReportBeautiful(allure_html_path=allure_report_dir, allure_results_path=allure_results_dir) + + # 设置报告窗口的标题 + allure_beautiful.set_windows_title( + new_title=kwargs.get("windows_title")) + + # 修改Allure报告Overview的标题文案 + allure_beautiful.set_report_name( + new_name=kwargs.get("report_name")) + + # 在allure-html报告中往widgets/environment.json中写入环境信息 + allure_beautiful.set_report_env_on_html( + env_info=kwargs.get("env_info")) + + # ----------------压缩allure测试报告,方便后续发送压缩包------------------------------------------ + # 复制http_server.exe以及双击打开Allure报告.bat,以便windows环境下,直接打开查看allure html报告 + allure_config_path = kwargs.get("allure_config_path") # 保存http_server.exe及双击打开Allure报告.bat的目录 + copy_file(src_file_path=os.path.join(allure_config_path, + [i for i in os.listdir(allure_config_path) if i.endswith(".exe")][0]), + dest_dir_path=allure_report_dir) + copy_file(src_file_path=os.path.join(allure_config_path, + [i for i in os.listdir(allure_config_path) if i.endswith(".bat")][0]), + dest_dir_path=allure_report_dir) + + attachment_path = kwargs.get("attachment_path") # allure报告压缩的路径,例如:report/allure_report.zip + zip_file(in_path=allure_report_dir, out_path=attachment_path) + + return allure_report_dir, attachment_path diff --git a/case_utils/get_results_handle.py b/case_utils/report_utils/get_results_handle.py similarity index 97% rename from case_utils/get_results_handle.py rename to case_utils/report_utils/get_results_handle.py index 30896a9..92005bb 100644 --- a/case_utils/get_results_handle.py +++ b/case_utils/report_utils/get_results_handle.py @@ -11,7 +11,7 @@ import json # 第三方库导入 from loguru import logger # 本地应用/模块导入 -from common_utils.time_handle import timestamp_strftime +from common_utils.other_utils.time_handle import timestamp_strftime def get_test_results_from_from_allure_report(allure_html_path): diff --git a/case_utils/platform_handle.py b/case_utils/report_utils/platform_handle.py similarity index 76% rename from case_utils/platform_handle.py rename to case_utils/report_utils/platform_handle.py index 3b42554..fda4533 100644 --- a/case_utils/platform_handle.py +++ b/case_utils/report_utils/platform_handle.py @@ -4,11 +4,13 @@ # @File : platform_handle.py # @Software: PyCharm # @Desc: 跨平台的支持allure,用于生成allure测试报告 + + # 标准库导入 import os.path import platform # 本地应用/模块导入 -from config.path_config import LIB_DIR, ALLURE_RESULTS_DIR, ALLURE_HTML_DIR +from config.path_config import LIB_DIR class PlatformHandle: @@ -21,9 +23,8 @@ class PlatformHandle: allure_path = os.path.join(allure_bin, "allure.bat") else: allure_path = os.path.join(allure_bin, "allure") - os.system(f"chmod +x {allure_path}") - cmd = f"{allure_path} generate {ALLURE_RESULTS_DIR} -o {ALLURE_HTML_DIR} --clean" - return cmd + os.popen(f"chmod +x {allure_path}").read() + return allure_path if __name__ == '__main__': diff --git a/case_utils/send_result_handle.py b/case_utils/report_utils/send_result_handle.py similarity index 91% rename from case_utils/send_result_handle.py rename to case_utils/report_utils/send_result_handle.py index 773f208..cb31c8f 100644 --- a/case_utils/send_result_handle.py +++ b/case_utils/report_utils/send_result_handle.py @@ -12,11 +12,11 @@ from loguru import logger from config.models import NotificationType from config.settings import SEND_RESULT_TYPE, email, ding_talk, wechat, email_subject, email_content, ding_talk_title, \ ding_talk_content, wechat_content -from case_utils.data_handle import data_handle -from case_utils.get_results_handle import get_test_results_from_from_allure_report -from common_utils.dingding_handle import DingTalkBot -from common_utils.wechat_handle import WechatBot -from common_utils.yagmail_handle import YagEmailServe +from case_utils.requests_utils.data_handle import data_handle +from case_utils.report_utils.get_results_handle import get_test_results_from_from_allure_report +from common_utils.notify_utils.dingding_handle import DingTalkBot +from common_utils.notify_utils.wechat_handle import WechatBot +from common_utils.notify_utils.yagmail_handle import YagEmailServe def send_email(user, pwd, host, subject, content, to, attachments): @@ -80,8 +80,7 @@ def send_result(report_path, attachment_path=None): """ # 默认不发送任何通知 if SEND_RESULT_TYPE == NotificationType.DEFAULT.value: - logger.info(f"SEND_RESULT_TYPE={SEND_RESULT_TYPE}, 配置了不发送任何邮件") - print(f"SEND_RESULT_TYPE={SEND_RESULT_TYPE}, 配置了不发送任何邮件") + logger.debug(f"SEND_RESULT_TYPE={SEND_RESULT_TYPE}, 配置了不发送任何邮件") return results = get_test_results_from_from_allure_report(report_path) diff --git a/case_utils/requests_utils/__init__.py b/case_utils/requests_utils/__init__.py new file mode 100644 index 0000000..486eaa7 --- /dev/null +++ b/case_utils/requests_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:41 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/case_utils/requests_utils/base_request.py b/case_utils/requests_utils/base_request.py new file mode 100644 index 0000000..811f9eb --- /dev/null +++ b/case_utils/requests_utils/base_request.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/10/12 13:41 +# @Author : chenyinhua +# @File : utils.py +# @Software: PyCharm +# @Desc: + +# 标准库导入 +import os +import time +# 第三方库导入 +import allure +from loguru import logger +import requests # pip install requests +from requests_toolbelt import MultipartEncoder # pip install requests_toolbelt + + +class BaseRequest: + """ + Request操作封装 + """ + + TIMEOUT = 5 + + session = None + + @classmethod + def get_session(cls): + """ + 单例模式保证测试过程中使用的都是一个session对象; + requests.session可以自动处理cookies,做状态保持。 + """ + if cls.session is None: + cls.session = requests.Session() + return cls.session + + @classmethod + @allure.step("请求各项参数:{req_data}") + def send_request(cls, req_data): + """ + 处理请求数据,转换成可用数据发送请求 + :param req_data: 请求数据 + :return: 响应对象 + """ + try: + before = "\n" + "=" * 80 \ + + "\n-------------Start:请求前--------------------\n" \ + f"标题: {req_data.get('title', None)}\n" \ + f"请求路径: {req_data.get('url', None)}\n" \ + f"请求方式: {req_data.get('method', None)}\n" \ + f"请求头: {req_data.get('headers', None)}\n" \ + f"请求Cookies: {req_data.get('cookies', None)}\n" \ + f"请求关键字: {req_data.get('request_type', None)}\n" \ + f"请求内容: {req_data.get('payload', None)}\n" \ + f"请求文件: {req_data.get('file', None)}\n" \ + + "=" * 80 + logger.info(before) + response_result = cls.send_api_request( + url=req_data.get("url"), + method=req_data.get("method").lower(), + request_type=req_data.get("request_type", None), + header=req_data.get("headers", None), + payload=req_data.get("payload", None), + files=req_data.get("files", None), + cookies=req_data.get("cookies", None) + ) + after = "\n" + "=" * 80 \ + + "\n-------------Start:请求后--------------------\n" \ + f"响应数据: {response_result.text}\n" \ + f"响应码: {response_result.status_code}\n" \ + f"响应耗时: {round(response_result.elapsed.total_seconds(), 2)} s || {round(response_result.elapsed.total_seconds() * 1000, 2)} ms\n" \ + + "=" * 80 + logger.info(after) + return response_result + except requests.exceptions.RequestException as e: + logger.error(f"请求出错,{str(e)}") + raise ValueError(f"请求出错,{str(e)}") + + @classmethod + def send_api_request(cls, url: str, method: str, request_type: str, header=None, payload=None, + files=None, cookies=None): + """ + 发送请求 + :param method: 请求方法 + :param url: 请求url + :param request_type: 请求参数类型,可选值为params,json,data + :param payload: 请求数据,对于不同请求类型,可以为dict,MultipartEncoder等 + :param files: 请求上传的文件 + :param header: 请求头 + :param cookies: 请求cookies + :return: 返回res对象 + """ + headers = header or {} + session = cls.get_session() + if request_type: + if request_type.lower() == 'params': + res = session.request(method=method, url=url, params=payload, headers=headers, cookies=cookies, + timeout=cls.TIMEOUT) + return res + elif request_type.lower() == 'data': + res = session.request(method=method, url=url, data=payload, headers=headers, cookies=cookies, + timeout=cls.TIMEOUT) + return res + elif request_type.lower() == 'json': + res = session.request(method=method, url=url, json=payload, headers=headers, cookies=cookies, + timeout=cls.TIMEOUT) + return res + elif request_type.lower() == 'file': + if files: + + file_name = os.path.basename(files) + encoder = MultipartEncoder(fields={"file": (file_name, open(files, "rb"))}, + boundary='------------------------' + str(time.time())) + headers['Content-Type'] = encoder.content_type + response = session.request(method=method, url=url, data=encoder.to_string(), headers=headers, + cookies=cookies, timeout=cls.TIMEOUT) + + return response + else: + logger.error("上传的文件不能为空") + else: + logger.error("request_type可选关键字为params, json, data, file") + raise ValueError('request_type可选关键字为params, json, data, file') + else: + logger.error("request_type参数不能为空") + raise ValueError('request_type参数不能为空') diff --git a/case_utils/case_data_analysis.py b/case_utils/requests_utils/case_data_analysis.py similarity index 97% rename from case_utils/case_data_analysis.py rename to case_utils/requests_utils/case_data_analysis.py index 16100f2..bed7774 100644 --- a/case_utils/case_data_analysis.py +++ b/case_utils/requests_utils/case_data_analysis.py @@ -8,7 +8,7 @@ # 标准库导入 from typing import Text # 第三方库导入 -from config.models import TestCase, TestCaseEnum, Method, RequestType, Severity +from config.models import TestCaseEnum, Method, RequestType, Severity class CaseDataCheck: @@ -65,7 +65,6 @@ class CaseDataCheck: 遍历一个枚举类中所有成员,并检查与每个成员对应的实例属性是否存在。 如果属性存在,则什么也不做,如果不存在,则抛出异常或执行其他操作 """ - print(f"打印:{list(TestCaseEnum._value2member_map_)}") for enum in list(TestCaseEnum._value2member_map_): if enum[1]: self.check_case_data_attr(enum[0]) diff --git a/case_utils/case_fun_handle.py b/case_utils/requests_utils/case_fun_handle.py similarity index 96% rename from case_utils/case_fun_handle.py rename to case_utils/requests_utils/case_fun_handle.py index ca01fb5..1192f3b 100644 --- a/case_utils/case_fun_handle.py +++ b/case_utils/requests_utils/case_fun_handle.py @@ -13,14 +13,14 @@ import re from xpinyin import Pinyin # 纯 Python 编写的中文字符串拼音转换模块,不需要依赖外部程序和词库。 from loguru import logger # 本地应用/模块导入 -from common_utils.excel_handle import ExcelHandle -from common_utils.yaml_handle import YamlHandle -from common_utils.files_handle import get_files, get_relative_path +from common_utils.read_files_utils.excel_handle import ExcelHandle +from common_utils.read_files_utils.yaml_handle import YamlHandle +from common_utils.other_utils.files_handle import get_files, get_relative_path from config.models import CaseFileType from config.settings import CASE_FILE_TYPE from config.path_config import DATA_DIR, CASE_TEMPLATE_DIR, AUTO_CASE_DIR from config.global_vars import CUSTOM_MARKERS -from case_utils.case_data_analysis import CaseDataCheck +from case_utils.requests_utils.case_data_analysis import CaseDataCheck """ 主要步骤: @@ -177,7 +177,7 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c os.makedirs(target_case_path, exist_ok=True) # 获取用例数据中的标记 case_markers = case_common.get("case_markers", []) or [] - logger.debug(f"从用例中拿到的标记有:{case_markers}, {type(case_markers)}") + logger.trace(f"从用例中拿到的标记有:{case_markers}, {type(case_markers)}") # 先读取用例模板中每一行的内容 with open(file=case_template_path, mode="r", encoding="utf-8") as f: case_template = f.readlines() @@ -187,7 +187,7 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c # 这里是预计往 @pytest.mark.parametrize( 这一行的上面插入标记 if content.strip().startswith('@pytest.mark.parametrize('): # 往测试用例模板中插入自定义标记 - logger.debug(f"获取到的case_markers:{case_markers}, {type(case_markers)}") + logger.trace(f"获取到的case_markers:{case_markers}, {type(case_markers)}") for case_marker in case_markers: # 获取符合要求格式的自定义标记名称,并插入到测试模板中 marker = is_valid_marker(case_marker) diff --git a/case_utils/data_handle.py b/case_utils/requests_utils/data_handle.py similarity index 100% rename from case_utils/data_handle.py rename to case_utils/requests_utils/data_handle.py diff --git a/case_utils/extract_data_handle.py b/case_utils/requests_utils/extract_data_handle.py similarity index 74% rename from case_utils/extract_data_handle.py rename to case_utils/requests_utils/extract_data_handle.py index 932bfd6..c5b2235 100644 --- a/case_utils/extract_data_handle.py +++ b/case_utils/requests_utils/extract_data_handle.py @@ -20,20 +20,18 @@ def json_extractor(obj: dict, expr: str = '.'): """ try: result = jsonpath(obj, expr)[0] - logger.debug("\n======================================================\n" \ + logger.trace("\n======================================================\n" \ "-------------Start:json_extractor--------------------\n" f"提取表达式为: {expr} \n" f"提取值为: {result}\n" "=====================================================") - print("提取响应内容成功,提取表达式为: {} 提取值为 {}".format(expr, result)) except Exception as e: - logger.debug("\n======================================================\n" \ + logger.trace("\n======================================================\n" \ "-------------End:json_extractor--------------------\n" f"提取表达式为: {expr}\n" f"提取数据为: {obj}\n" f"错误信息为:{e}\n" "=====================================================") - print(f'未提取到内容,请检查表达式是否错误!提取表达式为:{expr} 提取数据为 {obj}, 错误信息为:{e}') result = None return result @@ -46,19 +44,17 @@ def re_extract(obj: str, expr: str = '.'): """ try: result = re.findall(expr, obj)[0] - logger.debug("\n======================================================\n" \ + logger.trace("\n======================================================\n" \ "-------------Start:re_extract--------------------\n" f"提取表达式为: {expr}\n" \ f"提取值为: {result}\n" \ "=====================================================") - print("提取响应内容成功,提取表达式为: {} 提取值为: {}".format(expr, result)) except Exception as e: - logger.debug(f"\n======================================================\n" \ + logger.trace(f"\n======================================================\n" \ "-------------End:re_extract--------------------\n" f"提取表达式为: {expr}\n" \ f"提取数据为: {obj}\n" \ f"错误信息为:{e}\n" \ "=====================================================") - print(f'未提取到内容,请检查表达式是否错误!提取表达式为:{expr} 提取数据为 {obj}, 错误信息为:{e}') result = None return result diff --git a/case_utils/request_data_handle.py b/case_utils/requests_utils/request_data_handle.py similarity index 51% rename from case_utils/request_data_handle.py rename to case_utils/requests_utils/request_data_handle.py index 814db6d..89f60af 100644 --- a/case_utils/request_data_handle.py +++ b/case_utils/requests_utils/request_data_handle.py @@ -13,13 +13,12 @@ import http.cookiejar # 第三方库导入 from requests import Response from loguru import logger -import allure # 本地应用/模块导入 -from common_utils.files_handle import get_file_field -from common_utils.base_request import BaseRequest -from case_utils.data_handle import data_handle -from case_utils.extract_data_handle import json_extractor, re_extract -from case_utils.allure_handle import custom_allure_step +from common_utils.other_utils.files_handle import get_file_field +from case_utils.requests_utils.base_request import BaseRequest +from case_utils.requests_utils.data_handle import data_handle +from case_utils.requests_utils.extract_data_handle import json_extractor, re_extract +from case_utils.report_utils.allure_handle import allure_step from config.global_vars import GLOBAL_VARS from config.path_config import FILES_DIR @@ -32,10 +31,11 @@ class RequestPreDataHandle: """ def __init__(self, request_data): - logger.debug(f"\n======================================================\n" \ + logger.trace(f"\n======================================================\n" \ "-------------Start:处理用例数据前--------------------\n" f"用例标题(title): {type(request_data.get('title', None))} || {request_data.get('title', None)}\n" \ f"用例优先级(severity): {type(request_data.get('severity', None))} || {request_data.get('severity', None)}\n" \ + f"请求域名(host): {type(request_data.get('host', None))} || {request_data.get('host', None)}\n" \ f"请求路径(url): {type(request_data.get('url', None))} || {request_data.get('url', None)}\n" \ f"请求方式(method): {type(request_data.get('method', None))} || {request_data.get('method', None)}\n" \ f"请求头(headers): {type(request_data.get('headers', None))} || {request_data.get('headers', None)}\n" \ @@ -60,7 +60,7 @@ class RequestPreDataHandle: self.payload_handle() self.files_handle() self.assert_handle() - logger.debug(f"\n======================================================\n" \ + logger.trace(f"\n======================================================\n" \ "-------------End:处理用例数据后--------------------\n" f"用例标题(title): {type(self.request_data.get('title', None))} || {self.request_data.get('title', None)}\n" \ f"用例优先级(severity): {type(self.request_data.get('severity', None))} || {self.request_data.get('severity', None)}\n" \ @@ -78,35 +78,31 @@ class RequestPreDataHandle: return self.request_data def url_handle(self): - try: - """ - 用例数据中获取到的url(一般是不带host的,个别特殊的带有host,则不进行处理) - """ - # 检测url中是否存在需要替换的参数,如果存在则进行替换 - url = data_handle(obj=self.request_data.get("url", None), source=GLOBAL_VARS) - # 进行url处理,最终得到full_url - host = GLOBAL_VARS.get("host", "") - # 从用例数据中获取url,如果键url不存在,则返回空字符串 - # 如果url是以http开头的,则直接使用该url,不与host进行拼接 - if url.lower().startswith("http"): - full_url = url + """ + 用例数据中获取到的url(一般是不带host的,个别特殊的带有host,则不进行处理) + """ + # 检测url中是否存在需要替换的参数,如果存在则进行替换 + url = data_handle(obj=self.request_data.get("url", None), source=GLOBAL_VARS) + # 进行url处理,最终得到full_url + host = GLOBAL_VARS.get("host", "") + # 从用例数据中获取url,如果键url不存在,则返回空字符串 + # 如果url是以http开头的,则直接使用该url,不与host进行拼接 + if url.lower().startswith("http"): + full_url = url + else: + # 如果host以/结尾 并且 url以/开头 + if host.endswith("/") and url.startswith("/"): + full_url = host[0:len(host) - 1] + url + # 如果host以/结尾 并且 url不以/开头 + elif host.endswith("/") and (not url.startswith("/")): + full_url = host + url + elif (not host.endswith("/")) and url.startswith("/"): + # 如果host不以/结尾 且 url以/开头,则将host和url拼接起来,组成新的url + full_url = host + url else: - # 如果host以/结尾 并且 url以/开头 - if host.endswith("/") and url.startswith("/"): - full_url = host[0:len(host) - 1] + url - # 如果host以/结尾 并且 url不以/开头 - elif host.endswith("/") and (not url.startswith("/")): - full_url = host + url - elif (not host.endswith("/")) and url.startswith("/"): - # 如果host不以/结尾 且 url以/开头,则将host和url拼接起来,组成新的url - full_url = host + url - else: - # 如果host不以/结尾 且 url不以/开头,则将host和url拼接起来的时候增加/,组成新的url - full_url = host + "/" + url - self.request_data["url"] = full_url - except Exception as e: - logger.error(f"处理url报错了:{e}") - raise TypeError(f"处理url报错了:{e}") + # 如果host不以/结尾 且 url不以/开头,则将host和url拼接起来的时候增加/,组成新的url + full_url = host + "/" + url + self.request_data["url"] = full_url def method_handle(self): # TODO 暂时不需要处理,后续有需要在处理 @@ -121,7 +117,6 @@ class RequestPreDataHandle: # 从用例数据中获取cookies, 处理cookies if cookies: logger.debug(f"打印一下全局变量的值:{GLOBAL_VARS}") - print(f"打印一下全局变量的值:{GLOBAL_VARS}") # 通过全局变量替换cookies,得到的是一个str类型 cookies = data_handle(obj=cookies, source=GLOBAL_VARS) try: @@ -129,87 +124,62 @@ class RequestPreDataHandle: except Exception as e: cookies = cookies logger.debug(f"处理{cookies}报错了:{e}") - print(f"处理{cookies}报错了:{e}") if isinstance(cookies, dict) or isinstance(cookies, http.cookiejar.CookieJar): self.request_data["cookies"] = cookies else: - logger.error(f"cookies参数要求是Dict or CookieJar object, 目前cookies类型是:{type(cookies)}, cookies值是:{cookies}") - raise TypeError(f"cookies参数要求是Dict or CookieJar object, 目前cookies类型是:{type(cookies)}, cookies值是:{cookies}") + logger.error( + f"cookies参数要求是Dict or CookieJar object, 目前cookies类型是:{type(cookies)}, cookies值是:{cookies}") + raise TypeError( + f"cookies参数要求是Dict or CookieJar object, 目前cookies类型是:{type(cookies)}, cookies值是:{cookies}") def headers_handle(self): """ headers里面传cookies,要求cookies类型是str """ headers = self.request_data.get("headers", None) - try: - # 从用例数据中获取header, 处理header - if headers: - self.request_data["headers"] = data_handle(obj=headers, source=GLOBAL_VARS) - # 如果请求头中有cookies,需要进行单独处理 - if self.request_data["headers"].get("cookies", None): - cookies = self.request_data["headers"]["cookies"] - if isinstance(cookies, dict): - # 如果是字典类型,就转成字符串 - self.request_data["headers"]["cookies"] = json.dumps(cookies) - else: - self.request_data["headers"]["cookies"] = cookies - except Exception as e: - logger.error(f"处理{headers}报错了:{e}") - raise TypeError(f"处理{headers}报错了:{e}") + + # 从用例数据中获取header, 处理header + if headers: + self.request_data["headers"] = data_handle(obj=headers, source=GLOBAL_VARS) + # 如果请求头中有cookies,需要进行单独处理 + if self.request_data["headers"].get("cookies", None): + cookies = self.request_data["headers"]["cookies"] + if isinstance(cookies, dict): + # 如果是字典类型,就转成字符串 + self.request_data["headers"]["cookies"] = json.dumps(cookies) + else: + self.request_data["headers"]["cookies"] = cookies def payload_handle(self): # 处理请求参数payload payload = self.request_data.get("payload", None) - try: - if payload: - self.request_data["payload"] = data_handle(obj=payload, source=GLOBAL_VARS) - except Exception as e: - logger.error(f"处理{payload}报错了:{e}") - raise TypeError(f"处理{payload}报错了:{e}") + if payload: + self.request_data["payload"] = data_handle(obj=payload, source=GLOBAL_VARS) def files_handle(self): """ 格式:接口中文件参数的名称:"文件路径地址"/["文件地址1", "文件地址2"] - 例如:{"file": "test_demo.py"} + 例如:{"file": "demo_test_demo.py"} 或者 {"file": ["test_demo_01.py", "test_demo_02.py"]} """ # 处理请求参数files参数 files = self.request_data.get("files", None) - try: - if files: - for k, v in files.items(): - # ------------------ 处理多文件的情况 ------------------ - # 这里需要注意:不一定所有接口都支持多文件上传 - _files = [] - if isinstance(v, list): - for file in v: - # 处理文件绝对路径 - file_path = os.path.join(FILES_DIR, file) - # 多文件上传需要是元祖[('file', (filename, file_content)), ('file', (filename, file_content))] - _files.append((k, get_file_field(file_path))) - self.request_data["files"] = _files - else: - # ------------------ 处理单文件的情况 ------------------ - # 处理文件绝对路径 - file_path = os.path.join(FILES_DIR, v) - # 单文件上传需要是字典{'file': (filename, file_content)} - self.request_data["files"] = {k: get_file_field(file_path)} - logger.debug(f"处理完成后的file:{self.request_data['files']}") - except Exception as e: - logger.error(f"处理{files}报错了:{e}") - raise TypeError(f"处理{files}报错了:{e}") + if files: + for k, v in files.items(): + # ------------------ 单文件 ------------------ + # 处理文件绝对路径 + file_path = os.path.join(FILES_DIR, v) + # 单文件上传需要是字典{'file': (filename, file_content)} + self.request_data["files"] = {k: get_file_field(file_path)} + logger.debug(f"处理完成后的files:{self.request_data['files']}") def assert_handle(self): # 处理响应断言参数 assert_response = self.request_data.get("assert_response", None) - try: - if assert_response: - self.request_data["assert_response"] = data_handle(obj=assert_response, source=GLOBAL_VARS) - # 由于数据库断言里面的变量需要请求响应后进行提取,因此目前不进行处理 - except Exception as e: - logger.error(f"处理{assert_response}报错了:{e}") - raise TypeError(f"处理{assert_response}报错了:{e}") + if assert_response: + self.request_data["assert_response"] = data_handle(obj=assert_response, source=GLOBAL_VARS) + # 由于数据库断言里面的变量需要请求响应后进行提取,因此目前不进行处理 # ---------------------------------------- 进行请求,请求后的参数提取处理----------------------------------------# @@ -221,57 +191,26 @@ class RequestHandle: def __init__(self, case_data): self.case_data = case_data - @allure.step("发送请求") def http_request(self): """ 发送请求并进行后置参数提取操作 """ response = BaseRequest.send_request(self.case_data) + allure_step(f"响应数据: {response.text}") + allure_step(f"响应码: {response.status_code}") + allure_step( + f"响应耗时: {round(response.elapsed.total_seconds(), 2)} s || {round(response.elapsed.total_seconds() * 1000, 2)} ms") # 处理数据库断言 - 从全局变量中获取最新值,替换数据库断言中的参数 if self.case_data.get('assert_sql', None): self.case_data["assert_sql"] = data_handle(obj=self.case_data["assert_sql"], source=GLOBAL_VARS) - logger.info(f"\n======================================================\n" - "-------------执行请求获取响应数据--------------------\n" - f"用例标题(title): {type(self.case_data.get('title', None))} || {self.case_data.get('title', None)}\n" - f"请求路径(url): {type(self.case_data.get('url', None))} || {self.case_data.get('url', None)}\n" - f"请求方式(method): {type(self.case_data.get('method', None))} || {self.case_data.get('method', None)}\n" - f"请求头(headers): {type(self.case_data.get('headers', None))} || {self.case_data.get('headers', None)}\n" - f"请求cookies: {type(self.case_data.get('cookies', None))} || {self.case_data.get('cookies', None)}\n" - f"请求类型(request_type): {type(self.case_data.get('request_type', None))} || {self.case_data.get('request_type', None)}\n" - f"请求参数(payload): {type(self.case_data.get('payload', None))} || {self.case_data.get('payload', None)}\n") - custom_allure_step(step_title=f"请求地址(url):{self.case_data['url']}") - custom_allure_step(step_title=f"请求方式(method):{self.case_data['method']}") - custom_allure_step(step_title="请求头(headers)", content=self.case_data['headers']) - custom_allure_step(step_title="请求Cookies", content=str(self.case_data['cookies'])) - custom_allure_step(step_title="请求参数(payload)", content=self.case_data['payload']) # 处理请求里面的files,使得日志以及allure中写入的是文件,而不是文件二进制内容 if self.case_data.get('files', None): files = self.case_data["files"] - if isinstance(files, list): - for file in files: - _file = os.path.join(FILES_DIR, file[1][0]) - logger.info( - f"\n请求文件(files): {type(_file)} || {_file}\n") - custom_allure_step(step_title="请求文件(files)", source=_file) - elif isinstance(files, dict): + if isinstance(files, dict): dict_values = list(files.values())[0] _file = os.path.join(FILES_DIR, dict_values[0]) logger.info( f"\n请求文件(files): {type(_file)} || {_file}\n") - custom_allure_step(step_title="请求文件(files)", source=_file) - logger.info( - f"\n请求响应数据(response): {response.text}\n" - f"请求响应码(code): {response.status_code}\n" - f"响应耗时: {round(response.elapsed.total_seconds(), 2)} s || {round(response.elapsed.total_seconds() * 1000, 2)} ms\n" - "=====================================================") - try: - res = response.json() - custom_allure_step(step_title="请求响应数据(response)", content=res) - except: - custom_allure_step(step_title="请求响应数据(response)", content=response.text) - custom_allure_step(step_title=f"请求响应码(code):{response.status_code}") - custom_allure_step( - step_title=f"响应耗时:{round(response.elapsed.total_seconds(), 2)} s || {round(response.elapsed.total_seconds() * 1000, 2)} ms") return response @@ -289,19 +228,16 @@ def after_request_extract(response: Response, extract): "=====================================================") result = {} if extract: - try: - if response_type(response) == "json": - # 如果响应数据是json格式,则将按照json方式对后置提取参数进行处理 - res = response.json() - for k, v in extract.items(): - result[k] = json_extractor(res, v) - else: - # 如果响应数据是str格式,则将按照str方式对后置提取参数进行处理 - res = response.text - for k, v in extract.items(): - result[k] = re_extract(res, v) - except Exception as e: - logger.error(f"提取后置参数报错:{e}") + if response_type(response) == "json": + # 如果响应数据是json格式,则将按照json方式对后置提取参数进行处理 + res = response.json() + for k, v in extract.items(): + result[k] = json_extractor(res, v) + else: + # 如果响应数据是str格式,则将按照str方式对后置提取参数进行处理 + res = response.text + for k, v in extract.items(): + result[k] = re_extract(res, v) logger.debug(f"\n======================================================\n" \ "-------------End:从响应数据中提取后置参数保存到全局变量--------------------\n" f"后置提取参数(新): {result}\n" \ diff --git a/common_utils/base_request.py b/common_utils/base_request.py deleted file mode 100644 index 4b915cd..0000000 --- a/common_utils/base_request.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/6/22 14:11 -# @Author : chenyinhua -# @File : base_request.py -# @Software: PyCharm -# @Desc: 封装的requests模块 - -import time -from typing import Dict, Union -import requests -from loguru import logger -from requests import Response -from requests_toolbelt import MultipartEncoder - - -class BaseRequest: - """ - 进行请求 - """ - - TIMEOUT = 5 - - session = None - - @classmethod - def get_session(cls): - """ - 单例模式保证测试过程中使用的都是一个session对象; - requests.session可以自动处理cookies,做状态保持。 - """ - if cls.session is None: - cls.session = requests.Session() - return cls.session - - @classmethod - def send_request(cls, req_data: Dict[str, Union[str, Dict, MultipartEncoder]]) -> Response: - """ - 处理请求数据,转换成可用数据发送请求 - :param req_data: 请求数据 - :return: 响应对象 - """ - try: - logger.debug("\n" + "=" * 80 - + "\n-------------Start:请求前--------------------\n" - f"用例标题: {req_data.get('title', None)}\n" - f"请求路径: {req_data.get('url', None)}\n" - f"请求方式: {req_data.get('method', None)}\n" - f"请求头: {req_data.get('headers', None)}\n" - f"请求Cookies: {req_data.get('cookies', None)}\n" - f"请求关键字: {req_data.get('request_type', None)}\n" - f"请求内容: {req_data.get('payload', None)}\n" - f"请求文件: {req_data.get('files', None)}\n" - + "=" * 80) - print("\n" + "=" * 80 - + "\n-------------Start:请求前--------------------\n" - f"用例标题: {req_data.get('title', None)}\n" - f"请求路径: {req_data.get('url', None)}\n" - f"请求方式: {req_data.get('method', None)}\n" - f"请求头: {req_data.get('headers', None)}\n" - f"请求Cookies: {req_data.get('cookies', None)}\n" - f"请求关键字: {req_data.get('request_type', None)}\n" - f"请求内容: {req_data.get('payload', None)}\n" - f"请求文件: {req_data.get('files', None)}\n" - + "=" * 80) - res = cls.send_api_request( - url=req_data.get("url"), - method=req_data.get("method").lower(), - request_type=req_data.get("request_type", None), - header=req_data.get("headers", None), - payload=req_data.get("payload", None), - files=req_data.get("files", None), - cookies=req_data.get("cookies", None) - ) - logger.debug("\n" + "=" * 80 - + "\n-------------End:请求后--------------------\n" - f"响应数据: {res.text}\n" - f"响应码: {res.status_code}\n" - + "=" * 80) - print("\n" + "=" * 80 - + "\n-------------End:请求后--------------------\n" - f"响应数据: {res.text}\n" - f"响应码: {res.status_code}\n" - + "=" * 80) - except requests.exceptions.RequestException as e: - logger.error(f"请求出错,{str(e)}") - print(f"请求出错,{str(e)}") - raise ValueError(f"请求出错,{str(e)}") - - return res - - @classmethod - def send_api_request(cls, url: str, method: str, request_type: str, header: Dict[str, str] = None, payload=None, - files=None, cookies=None) -> Response: - """ - 发送请求 - :param method: 请求方法 - :param url: 请求url - :param request_type: 请求参数类型,可选值为params,json,data - :param payload: 请求数据,对于不同请求类型,可以为dict,MultipartEncoder等 - :param files: 请求上传的文件 - :param header: 请求头 - :param cookies: 请求cookies - :return: 返回res对象 - """ - headers = header or {} - session = cls.get_session() - - if request_type: - if request_type.lower() == 'params': - res = session.request(method=method, url=url, params=payload, headers=headers, cookies=cookies, - timeout=cls.TIMEOUT) - return res - elif request_type.lower() == 'data': - res = session.request(method=method, url=url, data=payload, headers=headers, cookies=cookies, - timeout=cls.TIMEOUT) - return res - elif request_type.lower() == 'json': - res = session.request(method=method, url=url, json=payload, headers=headers, cookies=cookies, - timeout=cls.TIMEOUT) - return res - elif request_type.lower() == 'file': - if files: - if payload: - if isinstance(files, dict): - for k, v in payload.items(): - files[k] = v - elif isinstance(files, list): - # TODO 这里是应对多文件上传的情况,暂时没有接口帮助验证是否真正上传了多个文件,有可能只上传成功了一个 - for k, v in payload.items(): - files.append((k, v)) - encoder = MultipartEncoder(fields=files, boundary='------------------------' + str(time.time())) - headers['Content-Type'] = encoder.content_type - res = session.request(method=method, url=url, data=encoder.to_string(), headers=headers, - cookies=cookies, timeout=cls.TIMEOUT) - return res - else: - logger.error('上传的文件不能为空') - print('上传的文件不能为空') - else: - logger.error('request_type可选关键字为params, json, data, file') - print('request_type可选关键字为params, json, data, file') - raise ValueError('request_type可选关键字为params, json, data, file') - else: - logger.error('request_type参数不能为空') - print('request_type参数不能为空') - raise ValueError('request_type参数不能为空') - - diff --git a/common_utils/database_utils/__init__.py b/common_utils/database_utils/__init__.py new file mode 100644 index 0000000..208fee5 --- /dev/null +++ b/common_utils/database_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:47 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/common_utils/mysql_handle.py b/common_utils/database_utils/mysql_handle.py similarity index 100% rename from common_utils/mysql_handle.py rename to common_utils/database_utils/mysql_handle.py diff --git a/common_utils/notify_utils/__init__.py b/common_utils/notify_utils/__init__.py new file mode 100644 index 0000000..9abd276 --- /dev/null +++ b/common_utils/notify_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:46 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/common_utils/dingding_handle.py b/common_utils/notify_utils/dingding_handle.py similarity index 100% rename from common_utils/dingding_handle.py rename to common_utils/notify_utils/dingding_handle.py diff --git a/common_utils/wechat_handle.py b/common_utils/notify_utils/wechat_handle.py similarity index 100% rename from common_utils/wechat_handle.py rename to common_utils/notify_utils/wechat_handle.py diff --git a/common_utils/yagmail_handle.py b/common_utils/notify_utils/yagmail_handle.py similarity index 100% rename from common_utils/yagmail_handle.py rename to common_utils/notify_utils/yagmail_handle.py diff --git a/common_utils/other_utils/__init__.py b/common_utils/other_utils/__init__.py new file mode 100644 index 0000000..9abd276 --- /dev/null +++ b/common_utils/other_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:46 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/common_utils/files_handle.py b/common_utils/other_utils/files_handle.py similarity index 100% rename from common_utils/files_handle.py rename to common_utils/other_utils/files_handle.py diff --git a/common_utils/func_handle.py b/common_utils/other_utils/func_handle.py similarity index 100% rename from common_utils/func_handle.py rename to common_utils/other_utils/func_handle.py diff --git a/common_utils/http_server.py b/common_utils/other_utils/http_server.py similarity index 100% rename from common_utils/http_server.py rename to common_utils/other_utils/http_server.py diff --git a/common_utils/time_handle.py b/common_utils/other_utils/time_handle.py similarity index 100% rename from common_utils/time_handle.py rename to common_utils/other_utils/time_handle.py diff --git a/common_utils/read_files_utils/__init__.py b/common_utils/read_files_utils/__init__.py new file mode 100644 index 0000000..35ea6ab --- /dev/null +++ b/common_utils/read_files_utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/11/10 14:45 +# @Author : floraachy +# @File : __init__.py +# @Software: PyCharm +# @Desc: diff --git a/common_utils/excel_handle.py b/common_utils/read_files_utils/excel_handle.py similarity index 100% rename from common_utils/excel_handle.py rename to common_utils/read_files_utils/excel_handle.py diff --git a/common_utils/yaml_handle.py b/common_utils/read_files_utils/yaml_handle.py similarity index 100% rename from common_utils/yaml_handle.py rename to common_utils/read_files_utils/yaml_handle.py diff --git a/config/case_template.txt b/config/case_template.txt index 76c84db..1f31352 100644 --- a/config/case_template.txt +++ b/config/case_template.txt @@ -9,9 +9,9 @@ from loguru import logger # 本地应用/模块导入 from config.settings import db_info from config.global_vars import GLOBAL_VARS -from case_utils.assert_handle import assert_response, assert_sql -from case_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract -from case_utils.allure_handle import allure_title +from case_utils.assert_utils.assert_handle import assert_response, assert_sql +from case_utils.requests_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract +from case_utils.report_utils.allure_handle import allure_title # 用例数据 diff --git a/config/global_vars.py b/config/global_vars.py index 78af19c..0cb3586 100644 --- a/config/global_vars.py +++ b/config/global_vars.py @@ -10,34 +10,3 @@ GLOBAL_VARS = {} # 定义一个变量。存储自定义的标记markers CUSTOM_MARKERS = [] - -ENV_VARS = { - "common": { - "报告标题": "自动化测试报告", - "项目名称": "GitLink 确实开源", - "测试人": "陈银花", - "所属部门": "开源中心" - }, - "test": { - # 示例测试环境及示例测试账号 - "host": "https://testforgeplus.trustie.net/", - "glcc_host": "https://testglcc.trustie.net", - "login": "auotest", - "password": "12345678", - "nickname": "AutoTest", - "user_id": "84954", - "project_id": "", - "project": "" - - }, - "live": { - "host": "https://www.gitlink.org.cn", - "glcc_host": "https://glcc.gitlink.org.cn", - "login": "******", - "password": "******", - "nickname": "******", - "user_id": "******", - "project_id": "", - "project": "" - } -} diff --git a/config/settings.py b/config/settings.py index c53c22f..a13e722 100644 --- a/config/settings.py +++ b/config/settings.py @@ -6,6 +6,58 @@ # @Software: PyCharm # @Desc: 项目配置文件 +# ------------------------------------ 测试数据配置 ----------------------------------------------------# +ENV_VARS = { + "common": { + "报告标题": "API自动化测试报告", + "项目名称": "GitLink 确实开源", + "测试人": "陈银花", + "所属部门": "开源中心" + }, + "test": { + # 示例测试环境及示例测试账号 + "host": "https://testforgeplus.trustie.net", + "login": "autotest", + "password": "***autotest***", # 运行时需要手动更改密码 + "nickname": "autotest", + "user_id": "106", + "super_login": "floraachy", + "super_password": "***floraachy***", + "project_id": "59", + "repo_id": "59", + "project_url": "/autotest/auotest" + + }, + "live": { + "host": "https://www.gitlink.org.cn", + "login": "autotest", + "password": "***autotest***", # 运行时需要手动更改密码 + "nickname": "autotest", + "user_id": "106", + "super_login": "floraachy", + "super_password": "***floraachy***", + "project_id": "", + "repo_id": "", + "project_url": "" + } +} + + +# ------------------------------------ pytest相关配置 ----------------------------------------------------# +class RunConfig: + """ + 运行测试配置 + """ + # 失败重跑次数 + rerun = 0 + + # 失败重跑间隔时间 + reruns_delay = 5 + + # 当达到最大失败数,停止执行 + max_fail = "10" + + # ------------------------------------ 配置信息 ----------------------------------------------------# # 0代表执行Excel和yaml两种格式的用例, 1 代表 yaml文件,2 用例代表Excel用例, 其他数值将不自动生成用例,仅能执行手动编写的用例 CASE_FILE_TYPE = 0 @@ -14,10 +66,9 @@ CASE_FILE_TYPE = 0 SEND_RESULT_TYPE = 0 # 指定日志收集级别 -LOG_LEVEL = None +LOG_LEVEL = "DEBUG" """ 支持的日志级别: - None: 表示捕获所有日志 TRACE: 最低级别的日志级别,用于详细追踪程序的执行。 DEBUG: 用于调试和开发过程中打印详细的调试信息。 INFO: 提供程序执行过程中的关键信息。 diff --git a/conftest.py b/conftest.py index 77be440..7d5498d 100644 --- a/conftest.py +++ b/conftest.py @@ -8,9 +8,12 @@ # 标准库导入 import time +import os +from datetime import datetime # 第三方库导入 from loguru import logger # 本地应用/模块导入 +from config.path_config import REPORT_DIR from config.global_vars import CUSTOM_MARKERS @@ -20,18 +23,14 @@ def pytest_configure(config): 注册自定义标记 """ # 注册自定义标记 - print(f"需要注册的标记:{CUSTOM_MARKERS}") logger.debug(f"需要注册的标记:{CUSTOM_MARKERS}") - markers = list(set(CUSTOM_MARKERS)) - for custom_marker in markers: + for custom_marker in CUSTOM_MARKERS: if isinstance(custom_marker, str): config.addinivalue_line('markers', f'{custom_marker}') - print(f"注册了自定义标记:{custom_marker}") logger.debug(f"注册了自定义标记:{custom_marker}") elif isinstance(custom_marker, dict): for k, v in custom_marker.items(): config.addinivalue_line('markers', f'{k}:{v}') - print(f"注册了自定义标记:{custom_marker}") logger.debug(f"注册了自定义标记:{custom_marker}") @@ -54,29 +53,36 @@ def pytest_terminal_summary(terminalreporter, config): _SKIPPED = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown']) _XPASSED = len([i for i in terminalreporter.stats.get('xpassed', []) if i.when != 'teardown']) _XFAILED = len([i for i in terminalreporter.stats.get('xfailed', []) if i.when != 'teardown']) + _TOTAL = terminalreporter._numcollected - _TIMES = time.time() - terminalreporter._sessionstarttime - _ACTUAL_RUN = _PASSED + _FAILED + _XPASSED + _XFAILED - logger.success(f"\n======================================================\n" - "-------------测试结果--------------------\n" - f"用例总数: {_TOTAL}\n" - f"跳过用例数: {_SKIPPED}\n" - f"实际执行用例总数: {_ACTUAL_RUN}\n" - f"通过用例数: {_PASSED}\n" - f"异常用例数: {_ERROR}\n" - f"失败用例数: {_FAILED}\n" - f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n" - f"意外通过的用例数: {_XPASSED}\n" - f"预期失败的用例数: {_XFAILED}\n\n" - "用例执行时长: %.2f" % _TIMES + " s\n") + + _DURATION = time.time() - terminalreporter._sessionstarttime + + session_start_time = datetime.fromtimestamp(terminalreporter._sessionstarttime) + _START_TIME = f"{session_start_time.year}年{session_start_time.month}月{session_start_time.day}日 " \ + f"{session_start_time.hour}:{session_start_time.minute}:{session_start_time.second}" + + test_info = f"各位同事, 大家好:\n" \ + f"自动化用例于 {_START_TIME}- 开始运行,运行时长:{_DURATION:.2f} s, 目前已执行完成。\n" \ + f"--------------------------------------\n" \ + f"#### 执行结果如下:\n" \ + f"- 用例运行总数: {_TOTAL} 个\n" \ + f"- 跳过用例个数(skipped): {_SKIPPED} 个\n" \ + f"- 实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED} 个\n" \ + f"- 通过用例个数(passed): {_PASSED} 个\n" \ + f"- 失败用例个数(failed): {_FAILED} 个\n" \ + f"- 异常用例个数(error): {_ERROR} 个\n" \ + f"- 重跑的用例数(--reruns的值): {_RERUN} ({reruns_value}) 个\n" try: - _RATE = _PASSED / _ACTUAL_RUN * 100 - logger.success( - f"\n用例成功率: %.2f" % _RATE + " %\n" - "=====================================================") + _RATE = _PASSED / (_TOTAL - _SKIPPED) * 100 + test_result = f"- 用例成功率: {_RATE:.2f} %\n" + logger.success(f"{test_info}{test_result}") except ZeroDivisionError: - logger.critical( - f"用例成功率: 0.00 %\n" - "=====================================================") + test_result = "- 用例成功率: 0.00 %\n" + logger.critical(f"{test_info}{test_result}") + + # 这里是方便在流水线里面发送测试结果到钉钉/企业微信的 + with open(file=os.path.join(REPORT_DIR, "test_result.txt"), mode="w", encoding="utf-8") as f: + f.write(f"{test_info}{test_result}") # ------------------------------------- END: pytest钩子函数处理---------------------------------------# diff --git a/data/gitlink/glcc/test_get_apply_information.yml b/data/gitlink/glcc/test_get_apply_information.yml index dd83f56..60fb752 100644 --- a/data/gitlink/glcc/test_get_apply_information.yml +++ b/data/gitlink/glcc/test_get_apply_information.yml @@ -1,10 +1,10 @@ case_common: - allure_epic: GitLink接口(自动生成用例) + allure_epic: GitLink接口 allure_feature: 开源夏令营模块 allure_story: 获取项目列表接口 case_markers: + - gitlink - glcc - - get_project - skip: 跳过执行该用例 case_info: diff --git a/data/gitlink/test_login.yaml b/data/gitlink/test_login.yaml new file mode 100644 index 0000000..0b4f60c --- /dev/null +++ b/data/gitlink/test_login.yaml @@ -0,0 +1,76 @@ +# 公共参数 +case_common: + allure_epic: GitLink接口 # 敏捷里面的概念,定义史诗,相当于module级的标签, 往下是 feature + allure_feature: 登录模块 # 功能点的描述,相当于class级的标签, 理解成模块往下是 story + allure_story: 登录接口 # 故事,可以理解为场景,相当于method级的标签, 往下是 title + case_markers: # pytest框架的标记 pytest.mark. + - gitlink + - login: 登录接口 + +# 用例数据 +case_info: +- + id: case_login_01 + title: 用户名密码正确,登录成功(不校验数据库) + run: True + severity: normal + url: /api/accounts/login.json + method: POST + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: { "login": "${login}","password": "${password}","autologin": 1 } + files: + extract: + nickname: $.username + login: $.login + user_id: $.user_id + assert_response: + eq: + http_code: 200 + $.user_id: ${user_id} + assert_sql: + +- + id: case_login_02 + title: 用户名密码正确,登录成功(校验数据库) + run: False + severity: minor + url: /api/accounts/login.json + method: POST + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: { "login": "${login}","password": "${password}","autologin": 1 } + files: + extract: + nickname: $.username + login: $.login + user_id: $.user_id + assert_response: + eq: + http_code: 200 + $.user_id: ${user_id} + assert_sql: + eq: + sql: select count(*) from tokens where user_id=${user_id}; + len: 1 + +- + id: case_login_03 + title: 用户名正确,密码错误,登录失败 + severity: critical + run: False + url: /api/accounts/login.json + method: POST + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: { "login": "chytest10","password": "password111","autologin": 1 } + files: + extract: + assert_response: + eq: + http_code: 200 + $.status: -2 + assert_sql: diff --git a/data/gitlink/test_new_project_demo.yaml b/data/gitlink/test_new_project_demo.yaml index c03bcef..23e212c 100644 --- a/data/gitlink/test_new_project_demo.yaml +++ b/data/gitlink/test_new_project_demo.yaml @@ -1,6 +1,6 @@ # 公共参数 case_common: - allure_epic: GitLink接口(自动生成用例) # 敏捷里面的概念,定义史诗,相当于module级的标签, 往下是 feature + allure_epic: GitLink接口 # 敏捷里面的概念,定义史诗,相当于module级的标签, 往下是 feature allure_feature: 开源项目模块 # 功能点的描述,相当于class级的标签, 理解成模块往下是 story allure_story: 新建项目接口 # 故事,可以理解为场景,相当于method级的标签, 往下是 title case_markers: @@ -24,7 +24,7 @@ case_info: request_type: json payload: "user_id": ${user_id} - "name": ${generate_name(len='zh')} + "name": ${generate_name(lan='zh')}_${generate_identifier()} "repository_name": ${generate_identifier()} files: extract: @@ -49,7 +49,7 @@ case_info: request_type: json payload: "user_id": ${user_id} - "name": ${generate_name(generate_name(len="zh"))} + "name": ${generate_name(lan='zh')}_${generate_identifier()} "repository_name": ${generate_identifier()} files: extract: @@ -74,7 +74,7 @@ case_info: request_type: json payload: "user_id": ${user_id} - "name": ${generate_name()} + "name": ${generate_name(lan='zh')}_${generate_identifier()} "repository_name": ${generate_identifier()} files: extract: @@ -90,56 +90,4 @@ case_info: sql: select id,`name`, identifier from projects where user_id=${user_id} ORDER BY created_on DESC; $.id: ${project_id} $.name: ${project_name} - $.identifier: ${project_identifier} - -- - id: case_new_project_04 - title: 正确输入各项必填参数,新建项目成功(04) - severity: normal - run: False - url: /api/projects.json - method: POST - headers: - Content-Type: application/json; charset=utf-8; - cookies: ${login_cookie} - request_type: json - payload: - "user_id": ${user_id} - "name": ${faker.name()} - "repository_name": ${generate_identifier()} - files: - extract: - project_id: $.id - project_name: $.name - project_identifier: $.identifier - assert_response: - eq: - http_code: 200 - $.login: ${login} - assert_sql: - -- - id: case_new_project_05 - title: 正确输入各项必填参数,新建项目成功(05) - severity: normal - run: False - url: /api/projects.json - method: POST - headers: - Content-Type: application/json; charset=utf-8; - cookies: ${login_cookie} - request_type: json - payload: - "user_id": ${user_id} - "name": ${fk_zh.name()} - "repository_name": ${generate_identifier()} - files: - extract: - project_id: $.id - project_name: $.name - project_identifier: $.identifier - assert_response: - eq: - http_code: 200 - $.login: ${login} - assert_sql: \ No newline at end of file + $.identifier: ${project_identifier} \ No newline at end of file diff --git a/data/gitlink/test_upload_files.yaml b/data/gitlink/test_upload_files.yaml index 3c5d343..5935df3 100644 --- a/data/gitlink/test_upload_files.yaml +++ b/data/gitlink/test_upload_files.yaml @@ -1,12 +1,11 @@ # 公共参数 case_common: - allure_epic: GitLink接口(自动生成用例) + allure_epic: GitLink接口 allure_feature: 上传文件模块 allure_story: 上传文件 case_markers: - gitlink - upload_file - - skip: 跳过该用例 # 用例数据 case_info: @@ -30,26 +29,3 @@ case_info: eq: http_code: 200 assert_sql: - -- - id: case_upload_file_02 - title: 测试多文件上传(该接口不支持多文件上传,这是一个示例) - severity: normal - run: False - url: /api/attachments.json - method: POST - headers: - cookies: ${login_cookie} - cookies: - request_type: file - payload: - files: - file: - - 导入TOC订单.xls - - toc.xls - extract: - file_id: $.id - assert_response: - eq: - http_code: 200 - assert_sql: \ No newline at end of file diff --git a/data/test_demo.yaml b/data/test_demo.yaml deleted file mode 100644 index 8a8be21..0000000 --- a/data/test_demo.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# 公共参数 -case_common: - allure_epic: 调试用例 - allure_feature: Debug模块 - allure_story: Debug接口 - case_markers: - -# 用例数据 -case_info: -- - id: case_demo_01 - title: 这是一条用于调试功能的用例 - run: True - severity: normal - url: /api/accounts/${FakerData.generate_time('%Y-%m-%d')}/login.json - method: POST - headers: {"Content-Type": "application/json; charset=utf-8;"} - cookies: - request_type: json - payload: - startTime: ${FakerData.generate_time('%Y-%m-%d')} - common2: ${faker.name()} - url: /api/accounts/${FakerData.generate_time('%Y-%m-%d')}/login.json - tripFragments: - - startTime: ${FakerData.generate_time('%Y-%m-%d')} - - common2: ${faker.name()} - - url: /api/accounts/${FakerData.generate_time('%Y-%m-%d')}/login.json - files: - extract: - assert_response: - eq: - http_code: 200 - assert_sql: \ No newline at end of file diff --git a/files/demo_get_apply_information.yml b/files/demo_get_apply_information.yml new file mode 100644 index 0000000..dd83f56 --- /dev/null +++ b/files/demo_get_apply_information.yml @@ -0,0 +1,47 @@ +case_common: + allure_epic: GitLink接口(自动生成用例) + allure_feature: 开源夏令营模块 + allure_story: 获取项目列表接口 + case_markers: + - glcc + - get_project + - skip: 跳过执行该用例 + +case_info: +- + id: case_glcc_demo_01 + title: 获取已报名成功的项目数据 + severity: normal + run: True + url: ${glcc_host}/api/applyInformation/list?curPage=1&pageSize=10000&round=2 + method: GET + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: + files: + extract: + assert_response: + eq: + http_code: 200 + $.message: success + assert_sql: + +- + id: case_glcc_demo_02 + title: 获取已报名成功的课题数据 + severity: normal + run: True + url: https://glcc.gitlink.org.cn/api/applyInformation/taskList?curPage=1&pageSize=20&userId=&round=2 + method: GET + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: + files: + extract: + assert_response: + eq: + http_code: 200 + $.message: success + assert_sql: \ No newline at end of file diff --git a/test_case/test_manual_case/test_demo.py b/files/demo_test_demo.py similarity index 86% rename from test_case/test_manual_case/test_demo.py rename to files/demo_test_demo.py index 89e76f9..973085f 100644 --- a/test_case/test_manual_case/test_demo.py +++ b/files/demo_test_demo.py @@ -2,7 +2,7 @@ # @Version: Python 3.9 # @Time : 2023/1/9 16:41 # @Author : chenyinhua -# @File : test_login_demo.py +# @File : demo_test_login.py # @Software: PyCharm # @Desc: python脚本编写的测试用例文件 @@ -11,7 +11,7 @@ import pytest from loguru import logger import allure # 本地应用/模块导入 -from case_utils.allure_handle import allure_title, custom_allure_step +from case_utils.report_utils.allure_handle import allure_title, allure_step # 读取用例数据 cases = [{"title": "demo用例01", "severity": "blocker1", "user": "flora", "age": 17, "run": True}, @@ -27,6 +27,6 @@ def test_demo(case): # 添加用例标题作为allure中显示的用例标题 allure_title(case.get("title", "")) # 在allure报告中显示请求的用例数据 - custom_allure_step(step_title="用例数据", content=f"{case}") + allure_step(step_title="用例数据", content=f"{case}") assert case["user"] == "flora" logger.info("\n-----------------------------END-用例执行结束-----------------------------\n") diff --git a/data/gitlink/test_gitlink_demo.xlsx b/files/demo_test_gitlink.xlsx similarity index 100% rename from data/gitlink/test_gitlink_demo.xlsx rename to files/demo_test_gitlink.xlsx diff --git a/test_case/test_manual_case/test_login_demo.py b/files/demo_test_login.py similarity index 82% rename from test_case/test_manual_case/test_login_demo.py rename to files/demo_test_login.py index ee7a50f..cbf13c5 100644 --- a/test_case/test_manual_case/test_login_demo.py +++ b/files/demo_test_login.py @@ -2,7 +2,7 @@ # @Version: Python 3.9 # @Time : 2023/1/9 16:41 # @Author : chenyinhua -# @File : test_login_demo.py +# @File : demo_test_login.py # @Software: PyCharm # @Desc: python脚本编写的测试用例文件 @@ -13,16 +13,16 @@ import pytest import allure from loguru import logger # 本地应用/模块导入 -from common_utils.yaml_handle import YamlHandle +from common_utils.read_files_utils.yaml_handle import YamlHandle from config.path_config import DATA_DIR from config.settings import db_info from config.global_vars import GLOBAL_VARS -from case_utils.assert_handle import assert_response, assert_sql -from case_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract -from case_utils.allure_handle import allure_title +from case_utils.assert_utils.assert_handle import assert_response, assert_sql +from case_utils.requests_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract +from case_utils.report_utils.allure_handle import allure_title # 读取用例数据 -yaml_data = YamlHandle(filename=os.path.join(DATA_DIR, "gitlink", "login_demo.yaml")).read_yaml +yaml_data = YamlHandle(filename=os.path.join(DATA_DIR, "gitlink", "test_login.yaml")).read_yaml case_common = yaml_data["case_common"] cases = yaml_data["case_info"] diff --git a/files/demo_test_new_project.yaml b/files/demo_test_new_project.yaml new file mode 100644 index 0000000..c03bcef --- /dev/null +++ b/files/demo_test_new_project.yaml @@ -0,0 +1,145 @@ +# 公共参数 +case_common: + allure_epic: GitLink接口(自动生成用例) # 敏捷里面的概念,定义史诗,相当于module级的标签, 往下是 feature + allure_feature: 开源项目模块 # 功能点的描述,相当于class级的标签, 理解成模块往下是 story + allure_story: 新建项目接口 # 故事,可以理解为场景,相当于method级的标签, 往下是 title + case_markers: + - gitlink + - new_project + - usefixtures: login_init + +# 用例数据 +case_info: +- + id: case_new_project_01 + title: 正确输入各项必填参数,新建项目成功(不校验数据库, header里面传cookies) + severity: critical + run: True + url: /api/projects.json + method: POST + headers: + Content-Type: application/json; charset=utf-8; + cookies: ${login_cookie} + cookies: + request_type: json + payload: + "user_id": ${user_id} + "name": ${generate_name(len='zh')} + "repository_name": ${generate_identifier()} + files: + extract: + project_id: $.id + project_name: $.name + project_identifier: $.identifier + assert_response: + eq: + http_code: 200 + $.login: ${login} + assert_sql: + +- + id: case_new_project_02 + title: 正确输入各项必填参数,新建项目成功(不校验数据库,单独传cookies) + run: True + url: /api/projects.json + method: POST + headers: + Content-Type: application/json; charset=utf-8; + cookies: ${login_cookie} + request_type: json + payload: + "user_id": ${user_id} + "name": ${generate_name(generate_name(len="zh"))} + "repository_name": ${generate_identifier()} + files: + extract: + project_id: $.id + project_name: $.name + project_identifier: $.identifier + assert_response: + eq: + http_code: 200 + $.login: ${login} + assert_sql: + +- + id: case_new_project_03 + title: 正确输入各项必填参数,新建项目成功(校验数据库) + severity: normal + run: False + url: /api/projects.json + method: POST + headers: {"Content-Type": "application/json; charset=utf-8;"} + cookies: + request_type: json + payload: + "user_id": ${user_id} + "name": ${generate_name()} + "repository_name": ${generate_identifier()} + files: + extract: + project_id: $.id + project_name: $.name + project_identifier: $.identifier + assert_response: + eq: + http_code: 200 + $.login: ${login} + assert_sql: + eq: + sql: select id,`name`, identifier from projects where user_id=${user_id} ORDER BY created_on DESC; + $.id: ${project_id} + $.name: ${project_name} + $.identifier: ${project_identifier} + +- + id: case_new_project_04 + title: 正确输入各项必填参数,新建项目成功(04) + severity: normal + run: False + url: /api/projects.json + method: POST + headers: + Content-Type: application/json; charset=utf-8; + cookies: ${login_cookie} + request_type: json + payload: + "user_id": ${user_id} + "name": ${faker.name()} + "repository_name": ${generate_identifier()} + files: + extract: + project_id: $.id + project_name: $.name + project_identifier: $.identifier + assert_response: + eq: + http_code: 200 + $.login: ${login} + assert_sql: + +- + id: case_new_project_05 + title: 正确输入各项必填参数,新建项目成功(05) + severity: normal + run: False + url: /api/projects.json + method: POST + headers: + Content-Type: application/json; charset=utf-8; + cookies: ${login_cookie} + request_type: json + payload: + "user_id": ${user_id} + "name": ${fk_zh.name()} + "repository_name": ${generate_identifier()} + files: + extract: + project_id: $.id + project_name: $.name + project_identifier: $.identifier + assert_response: + eq: + http_code: 200 + $.login: ${login} + assert_sql: \ No newline at end of file diff --git a/files/demo_test_upload.yaml b/files/demo_test_upload.yaml new file mode 100644 index 0000000..3c5d343 --- /dev/null +++ b/files/demo_test_upload.yaml @@ -0,0 +1,55 @@ +# 公共参数 +case_common: + allure_epic: GitLink接口(自动生成用例) + allure_feature: 上传文件模块 + allure_story: 上传文件 + case_markers: + - gitlink + - upload_file + - skip: 跳过该用例 + +# 用例数据 +case_info: +- + id: case_upload_file_01 + title: 测试单文件上传 + severity: + run: False + url: /api/attachments.json + method: POST + headers: + cookies: ${login_cookie} + cookies: + request_type: file + payload: + files: + file: TOC出库订单导入模板(2).xlsx + extract: + file_id: $.id + assert_response: + eq: + http_code: 200 + assert_sql: + +- + id: case_upload_file_02 + title: 测试多文件上传(该接口不支持多文件上传,这是一个示例) + severity: normal + run: False + url: /api/attachments.json + method: POST + headers: + cookies: ${login_cookie} + cookies: + request_type: file + payload: + files: + file: + - 导入TOC订单.xls + - toc.xls + extract: + file_id: $.id + assert_response: + eq: + http_code: 200 + assert_sql: \ No newline at end of file diff --git a/files/gitlinklogo2.png b/files/gitlinklogo2.png new file mode 100644 index 0000000..378b197 Binary files /dev/null and b/files/gitlinklogo2.png differ diff --git a/files/gitlinklogo3.jpg b/files/gitlinklogo3.jpg new file mode 100644 index 0000000..c27e2a4 Binary files /dev/null and b/files/gitlinklogo3.jpg differ diff --git a/data/gitlink/login_demo.yaml b/files/login_demo.yaml similarity index 100% rename from data/gitlink/login_demo.yaml rename to files/login_demo.yaml diff --git a/run.py b/run.py index 8ef2154..a0afb23 100644 --- a/run.py +++ b/run.py @@ -9,10 +9,11 @@ 说明: 1、用例创建原则,测试文件名必须以“test”开头,测试函数必须以“test”开头。 2、运行方式: - > python run.py (默认在test环境运行测试用例, 报告采用allure) - > python run.py -m demo 在test环境仅运行打了标记demo用例, 默认报告采用allure + > python run.py 默认在test环境运行测试用例, 生成allure测试报告 + > python run.py -m demo 在test环境仅运行打了标记demo用例,生成allure测试报告 > python run.py -env live 在live环境运行测试用例 > python run.py -env=test 在test环境运行测试用例 + > python run.py -report=no 在test环境下允许测试用例,不生成allure测试报告 pytest相关参数:以下也可通过pytest.ini配置 --reruns: 失败重跑次数 @@ -34,147 +35,81 @@ pytest相关参数:以下也可通过pytest.ini配置 # 标准库导入 import os import shutil -from datetime import datetime # 第三方库导入 import pytest from loguru import logger import click # 本地应用/模块导入 -from case_utils.case_fun_handle import generate_cases -from case_utils.platform_handle import PlatformHandle -from case_utils.send_result_handle import send_result -from case_utils.allure_handle import AllureReportBeautiful -from config.path_config import REPORT_DIR, LOG_DIR, AUTO_CASE_DIR, CONF_DIR, ALLURE_RESULTS_DIR, \ - ALLURE_HTML_DIR -from config.settings import LOG_LEVEL -from config.global_vars import GLOBAL_VARS, ENV_VARS -from common_utils.files_handle import zip_file, copy_file - - -def capture_all_logs(level=LOG_LEVEL): - logger.info(""" - _ _ _ _____ _ - __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_ - / _` | "_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __| - | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_ - \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__| - |_| - Starting ... ... ... - """) - if level: - # 仅捕获指定级别日志 - logger.add( - os.path.join(LOG_DIR, "runtime_{time}.log"), - enqueue=True, - encoding="utf-8", - rotation="00:00", - level=LOG_LEVEL.upper(), - format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", - ) - else: - # 捕获所有日志 - logger.add( - os.path.join(LOG_DIR, "runtime_{time}_all.log"), - enqueue=True, - encoding="utf-8", - rotation="00:00", - format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", - ) - - -# 封装生成测试用例的函数 -def auto_generate_test_cases(): - # 删除原有的测试用例,以便生成新的测试用例 - if os.path.exists(AUTO_CASE_DIR): - shutil.rmtree(AUTO_CASE_DIR) - - # 根据data里面的yaml/excel文件,自动生成测试用例 - generate_cases() - - -# 封装执行 pytest 的函数 -def run_pytest(mark_param): - arg_list = [] - - # 执行指定的测试用例 - if mark_param is not None: - arg_list.append(f"-m {mark_param}") - - current_time = datetime.now().strftime("%Y-%m-%d+%H_%M_%S") - - # 生成 Allure 报告 - arg_list.extend( - [ - "-q", - "--cache-clear", - f'--alluredir={ALLURE_RESULTS_DIR}', - "--clean-alluredir", - ] - ) - pytest.main(args=arg_list) - - # ------------------------ 使用allure生成测试报告 ------------------------ - allure_cmd = PlatformHandle().allure - os.popen(allure_cmd).read() - # ------------------------ 美化allure测试报告 ------------------------ - # 设置allure报告窗口标题 - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_windows_title( - new_title=ENV_VARS["common"]["项目名称"] - ) - # 设置allure报告名称 - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_name( - new_name=ENV_VARS["common"]["报告标题"] - ) - # 往allure测试报告中写入环境配置相关信息 - env_info = ENV_VARS["common"] - env_info["运行环境"] = GLOBAL_VARS.get("host", "") - AllureReportBeautiful(allure_html_path=ALLURE_HTML_DIR).set_report_env_on_html( - env_info=env_info - ) - # 复制http_server.exe以及双击查看报告.bat文件到allure-html根目录下,用于支撑电脑在未安装allure服务的情况下打开allure-html报告 - # 注意:ZIP文件的名称包含某些特殊字符,会导致无法使用.bat文件打开allure-html报告, 例如空格,/ 等 - allure_config_path = os.path.join(CONF_DIR, "allure_config") - - copy_file(src_file_path=os.path.join(allure_config_path, - [i for i in os.listdir(allure_config_path) if i.endswith(".exe")][0]), - dest_dir_path=ALLURE_HTML_DIR) - copy_file(src_file_path=os.path.join(allure_config_path, - [i for i in os.listdir(allure_config_path) if i.endswith(".bat")][0]), - dest_dir_path=ALLURE_HTML_DIR) - - # ------------------------ allure测试报告生成完毕,压缩allure测试报告为ZIP文件 ------------------------ - # report_path以及attachment_path,后面发送测试结果需要用到 - report_path = ALLURE_HTML_DIR - attachment_path = os.path.join(REPORT_DIR, f'autotest_{str(current_time)}.zip') - # 压缩allure-html报告为一个压缩文件zip - zip_file(in_path=report_path, out_path=attachment_path) - return report_path, attachment_path +from case_utils.requests_utils.case_fun_handle import generate_cases +from case_utils.report_utils.send_result_handle import send_result +from config.path_config import REPORT_DIR, LOG_DIR, CONF_DIR, ALLURE_RESULTS_DIR, ALLURE_HTML_DIR, AUTO_CASE_DIR +from config.settings import LOG_LEVEL, RunConfig, ENV_VARS +from config.global_vars import GLOBAL_VARS +from case_utils.logger_utils.log_handle import capture_logs +from case_utils.report_utils.allure_handle import generate_allure_report # 主函数 @click.command() +@click.option("-report", default="yes", help="是否生成allure html report,支持如下类型:yes, no") @click.option("-env", default="test", help="输入运行环境:test 或 live") @click.option("-m", default=None, help="选择需要运行的用例:python.ini配置的名称") -def run(env, m): +def run(env, m, report): try: # ------------------------ 捕获日志---------------------------- - capture_all_logs() + capture_logs(level=LOG_LEVEL, filename=os.path.join(LOG_DIR, "service.log")) + + logger.info("""\n\n + _ _ _ _____ _ + __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_ + / _` | "_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __| + | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_ + \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__| + |_| + Starting ... ... ... + """) + + # ------------------------ 处理一下获取到的参数---------------------------- + print(f"打印一下run方法的入参:\nreport={report}\nenv={env}\nm={m}") - # ------------------------ 设置全局变量 ------------------------ # 根据指定的环境参数,将运行环境所需相关配置数据保存到GLOBAL_VARS GLOBAL_VARS["env_key"] = env.lower() if ENV_VARS.get(env.lower()): GLOBAL_VARS.update(ENV_VARS[env.lower()]) # ------------------------ 自动生成测试用例 ------------------------ - auto_generate_test_cases() + # 删除原有的测试用例,以便生成新的测试用例 + if os.path.exists(AUTO_CASE_DIR): + shutil.rmtree(AUTO_CASE_DIR) + + # 根据data里面的yaml/excel文件,自动生成测试用例 + generate_cases() + + # ------------------------ 设置pytest相关参数 ------------------------ + arg_list = [f"--maxfail={RunConfig.max_fail}", f"--reruns={RunConfig.rerun}", + f"--reruns-delay={RunConfig.reruns_delay}", f'--alluredir={ALLURE_RESULTS_DIR}', + '--clean-alluredir'] + if m: + arg_list.append(f"-m {m}") # ------------------------ pytest执行测试用例 ------------------------ - report_path, attachment_path = run_pytest(mark_param=m) + print(f"打印一下运行的参数:{arg_list}") + pytest.main(args=arg_list) + # ------------------------ 生成测试报告 ------------------------ + if report == "yes": + report_path, attachment_path = generate_allure_report(allure_results=ALLURE_RESULTS_DIR, + allure_report=ALLURE_HTML_DIR, + windows_title=ENV_VARS["common"]["项目名称"], + report_name=ENV_VARS["common"]["报告标题"], + env_info={ + "运行环境": GLOBAL_VARS.get("host", None)}, + allure_config_path=os.path.join(CONF_DIR, + "allure_config"), + attachment_path=os.path.join(REPORT_DIR, + f'autotest_report.zip')) + # ------------------------ 发送测试结果 ------------------------ - # ------------------------ 发送测试结果 ------------------------ - # 发送通知 - send_result(report_path, attachment_path=attachment_path) + send_result(report_path=report_path, attachment_path=attachment_path) except Exception as e: raise e diff --git a/run_no_html_report.py b/run_no_html_report.py deleted file mode 100644 index 47d5ee6..0000000 --- a/run_no_html_report.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -# @Time : 2023/9/29 9:04 -# @Author : Flora.Chen -# @File : run_no_html_report.py -# @Software: PyCharm -# @Desc: 框架主入口 - -""" -说明: -1、用例创建原则,测试文件名必须以“test”开头,测试函数必须以“test”开头。 -2、运行方式: - > python run.py (默认在test环境运行测试用例, 报告采用allure) - > python run.py -m demo 在test环境仅运行打了标记demo用例, 默认报告采用allure - > python run.py -env live 在live环境运行测试用例 - > python run.py -env=test 在test环境运行测试用例 - -pytest相关参数:以下也可通过pytest.ini配置 - --reruns: 失败重跑次数 - --reruns-delay 失败重跑间隔时间 - --count: 重复执行次数 - -v: 显示错误位置以及错误的详细信息 - -s: 等价于 pytest --capture=no 可以捕获print函数的输出 - -q: 简化输出信息 - -m: 运行指定标签的测试用例 - -x: 一旦错误,则停止运行 - --cache-clear 清除pytest的缓存,包括测试结果缓存、抓取的fixture实例缓存和收集器信息缓存等 - --maxfail: 设置最大失败次数,当超出这个阈值时,则不会在执行测试用例 - "--reruns=3", "--reruns-delay=2" - - allure相关参数: - –-alluredir这个选项用于指定存储测试结果的路径 -""" - -# 标准库导入 -import os -import shutil -from datetime import datetime -# 第三方库导入 -import pytest -from loguru import logger -import click -# 本地应用/模块导入 -from case_utils.case_fun_handle import generate_cases -from config.path_config import LOG_DIR, AUTO_CASE_DIR, ALLURE_RESULTS_DIR -from config.settings import LOG_LEVEL -from config.global_vars import GLOBAL_VARS, ENV_VARS - - -def capture_all_logs(level=LOG_LEVEL): - logger.info(""" - _ _ _ _____ _ - __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_ - / _` | "_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __| - | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_ - \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__| - |_| - Starting ... ... ... - """) - if level: - # 仅捕获指定级别日志 - logger.add( - os.path.join(LOG_DIR, "runtime_{time}.log"), - enqueue=True, - encoding="utf-8", - rotation="00:00", - level=LOG_LEVEL.upper(), - format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", - ) - else: - # 捕获所有日志 - logger.add( - os.path.join(LOG_DIR, "runtime_{time}_all.log"), - enqueue=True, - encoding="utf-8", - rotation="00:00", - format="{time:YYYY-MM-DD HH:mm:ss} {level} From {module}.{function} : {message}", - ) - - -# 封装生成测试用例的函数 -def auto_generate_test_cases(): - # 删除原有的测试用例,以便生成新的测试用例 - if os.path.exists(AUTO_CASE_DIR): - shutil.rmtree(AUTO_CASE_DIR) - - # 根据data里面的yaml/excel文件,自动生成测试用例 - generate_cases() - - -# 封装执行 pytest 的函数 -def run_pytest(mark_param): - arg_list = [] - - # 执行指定的测试用例 - if mark_param is not None: - arg_list.append(f"-m {mark_param}") - - current_time = datetime.now().strftime("%Y-%m-%d+%H_%M_%S") - - # 生成 Allure 报告 - arg_list.extend( - [ - "-q", - "--cache-clear", - f'--alluredir={ALLURE_RESULTS_DIR}', - "--clean-alluredir", - ] - ) - pytest.main(args=arg_list) - - -# 主函数 -@click.command() -@click.option("-env", default="test", help="输入运行环境:test 或 live") -@click.option("-m", default=None, help="选择需要运行的用例:python.ini配置的名称") -def run(env, m): - try: - # ------------------------ 捕获日志---------------------------- - capture_all_logs() - - # ------------------------ 设置全局变量 ------------------------ - # 根据指定的环境参数,将运行环境所需相关配置数据保存到GLOBAL_VARS - GLOBAL_VARS["env_key"] = env.lower() - if ENV_VARS.get(env.lower()): - GLOBAL_VARS.update(ENV_VARS[env.lower()]) - - # ------------------------ 自动生成测试用例 ------------------------ - auto_generate_test_cases() - - # ------------------------ pytest执行测试用例 ------------------------ - run_pytest(mark_param=m) - - except Exception as e: - raise e - - -if __name__ == "__main__": - run() diff --git a/test_case/conftest.py b/test_case/conftest.py index 4197355..74f36a1 100644 --- a/test_case/conftest.py +++ b/test_case/conftest.py @@ -14,7 +14,7 @@ import allure from loguru import logger # 本地应用/模块导入 from config.global_vars import GLOBAL_VARS -from common_utils.base_request import BaseRequest +from case_utils.requests_utils.base_request import BaseRequest @pytest.fixture(scope="function", autouse=True)