From 4943adf36bf6180d2d92ac9ca413c7ff900dc512 Mon Sep 17 00:00:00 2001 From: floraachy <1622042529@qq.com> Date: Fri, 10 Nov 2023 15:58:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=B8=80=E7=B3=BB=E5=88=97?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E4=B8=94=E5=88=A0=E9=99=A4=E6=89=80?= =?UTF-8?q?=E6=9C=89demo=E7=94=A8=E4=BE=8B=E4=BB=85=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E7=9A=84=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 100 ++------ case_utils/assert_utils/__init__.py | 6 + .../{ => assert_utils}/assert_handle.py | 36 ++- case_utils/logger_utils/__init__.py | 6 + case_utils/logger_utils/log_handle.py | 60 +++++ case_utils/report_utils/__init__.py | 6 + .../{ => report_utils}/allure_handle.py | 56 ++++- .../{ => report_utils}/get_results_handle.py | 2 +- .../{ => report_utils}/platform_handle.py | 9 +- .../{ => report_utils}/send_result_handle.py | 13 +- case_utils/requests_utils/__init__.py | 6 + case_utils/requests_utils/base_request.py | 126 ++++++++++ .../case_data_analysis.py | 3 +- .../{ => requests_utils}/case_fun_handle.py | 12 +- .../{ => requests_utils}/data_handle.py | 0 .../extract_data_handle.py | 12 +- .../request_data_handle.py | 218 +++++++----------- common_utils/base_request.py | 148 ------------ common_utils/database_utils/__init__.py | 6 + .../{ => database_utils}/mysql_handle.py | 0 common_utils/notify_utils/__init__.py | 6 + .../{ => notify_utils}/dingding_handle.py | 0 .../{ => notify_utils}/wechat_handle.py | 0 .../{ => notify_utils}/yagmail_handle.py | 0 common_utils/other_utils/__init__.py | 6 + .../{ => other_utils}/files_handle.py | 0 common_utils/{ => other_utils}/func_handle.py | 0 common_utils/{ => other_utils}/http_server.py | 0 common_utils/{ => other_utils}/time_handle.py | 0 common_utils/read_files_utils/__init__.py | 6 + .../{ => read_files_utils}/excel_handle.py | 0 .../{ => read_files_utils}/yaml_handle.py | 0 config/case_template.txt | 6 +- config/global_vars.py | 31 --- config/settings.py | 55 ++++- conftest.py | 58 ++--- .../glcc/test_get_apply_information.yml | 4 +- data/gitlink/test_login.yaml | 76 ++++++ data/gitlink/test_new_project_demo.yaml | 62 +---- data/gitlink/test_upload_files.yaml | 26 +-- data/test_demo.yaml | 33 --- files/demo_get_apply_information.yml | 47 ++++ .../test_demo.py => files/demo_test_demo.py | 6 +- .../demo_test_gitlink.xlsx | Bin .../demo_test_login.py | 12 +- files/demo_test_new_project.yaml | 145 ++++++++++++ files/demo_test_upload.yaml | 55 +++++ files/gitlinklogo2.png | Bin 0 -> 7206 bytes files/gitlinklogo3.jpg | Bin 0 -> 7462 bytes {data/gitlink => files}/login_demo.yaml | 0 run.py | 175 +++++--------- run_no_html_report.py | 138 ----------- test_case/conftest.py | 2 +- 53 files changed, 906 insertions(+), 868 deletions(-) create mode 100644 case_utils/assert_utils/__init__.py rename case_utils/{ => assert_utils}/assert_handle.py (82%) create mode 100644 case_utils/logger_utils/__init__.py create mode 100644 case_utils/logger_utils/log_handle.py create mode 100644 case_utils/report_utils/__init__.py rename case_utils/{ => report_utils}/allure_handle.py (69%) rename case_utils/{ => report_utils}/get_results_handle.py (97%) rename case_utils/{ => report_utils}/platform_handle.py (76%) rename case_utils/{ => report_utils}/send_result_handle.py (91%) create mode 100644 case_utils/requests_utils/__init__.py create mode 100644 case_utils/requests_utils/base_request.py rename case_utils/{ => requests_utils}/case_data_analysis.py (97%) rename case_utils/{ => requests_utils}/case_fun_handle.py (96%) rename case_utils/{ => requests_utils}/data_handle.py (100%) rename case_utils/{ => requests_utils}/extract_data_handle.py (74%) rename case_utils/{ => requests_utils}/request_data_handle.py (51%) delete mode 100644 common_utils/base_request.py create mode 100644 common_utils/database_utils/__init__.py rename common_utils/{ => database_utils}/mysql_handle.py (100%) create mode 100644 common_utils/notify_utils/__init__.py rename common_utils/{ => notify_utils}/dingding_handle.py (100%) rename common_utils/{ => notify_utils}/wechat_handle.py (100%) rename common_utils/{ => notify_utils}/yagmail_handle.py (100%) create mode 100644 common_utils/other_utils/__init__.py rename common_utils/{ => other_utils}/files_handle.py (100%) rename common_utils/{ => other_utils}/func_handle.py (100%) rename common_utils/{ => other_utils}/http_server.py (100%) rename common_utils/{ => other_utils}/time_handle.py (100%) create mode 100644 common_utils/read_files_utils/__init__.py rename common_utils/{ => read_files_utils}/excel_handle.py (100%) rename common_utils/{ => read_files_utils}/yaml_handle.py (100%) create mode 100644 data/gitlink/test_login.yaml delete mode 100644 data/test_demo.yaml create mode 100644 files/demo_get_apply_information.yml rename test_case/test_manual_case/test_demo.py => files/demo_test_demo.py (86%) rename data/gitlink/test_gitlink_demo.xlsx => files/demo_test_gitlink.xlsx (100%) rename test_case/test_manual_case/test_login_demo.py => files/demo_test_login.py (82%) create mode 100644 files/demo_test_new_project.yaml create mode 100644 files/demo_test_upload.yaml create mode 100644 files/gitlinklogo2.png create mode 100644 files/gitlinklogo3.jpg rename {data/gitlink => files}/login_demo.yaml (100%) delete mode 100644 run_no_html_report.py 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 0000000000000000000000000000000000000000..378b19792c888e653d3df4cf2c7dfec61ded9f29 GIT binary patch literal 7206 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 C8J%|k literal 0 HcmV?d00001 diff --git a/files/gitlinklogo3.jpg b/files/gitlinklogo3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c27e2a4f2405f8b7709166f731aaab24d034e888 GIT binary patch literal 7462 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 literal 0 HcmV?d00001 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)