进行一系列优化,且删除所有demo用例仅保持正常的示例
This commit is contained in:
100
README.md
100
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)
|
||||
|
||||
|
||||
6
case_utils/assert_utils/__init__.py
Normal file
6
case_utils/assert_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:40
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
@@ -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}")
|
||||
6
case_utils/logger_utils/__init__.py
Normal file
6
case_utils/logger_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:42
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
60
case_utils/logger_utils/log_handle.py
Normal file
60
case_utils/logger_utils/log_handle.py
Normal file
@@ -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}")
|
||||
6
case_utils/report_utils/__init__.py
Normal file
6
case_utils/report_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:39
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
@@ -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
|
||||
@@ -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):
|
||||
@@ -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__':
|
||||
@@ -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)
|
||||
6
case_utils/requests_utils/__init__.py
Normal file
6
case_utils/requests_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:41
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
126
case_utils/requests_utils/base_request.py
Normal file
126
case_utils/requests_utils/base_request.py
Normal file
@@ -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参数不能为空')
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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" \
|
||||
@@ -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参数不能为空')
|
||||
|
||||
|
||||
6
common_utils/database_utils/__init__.py
Normal file
6
common_utils/database_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:47
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
6
common_utils/notify_utils/__init__.py
Normal file
6
common_utils/notify_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:46
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
6
common_utils/other_utils/__init__.py
Normal file
6
common_utils/other_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:46
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
6
common_utils/read_files_utils/__init__.py
Normal file
6
common_utils/read_files_utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time : 2023/11/10 14:45
|
||||
# @Author : floraachy
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
# @Desc:
|
||||
@@ -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
|
||||
|
||||
|
||||
# 用例数据
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 提供程序执行过程中的关键信息。
|
||||
|
||||
58
conftest.py
58
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钩子函数处理---------------------------------------#
|
||||
|
||||
@@ -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:
|
||||
|
||||
76
data/gitlink/test_login.yaml
Normal file
76
data/gitlink/test_login.yaml
Normal file
@@ -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:
|
||||
@@ -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:
|
||||
$.identifier: ${project_identifier}
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
47
files/demo_get_apply_information.yml
Normal file
47
files/demo_get_apply_information.yml
Normal file
@@ -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:
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
|
||||
145
files/demo_test_new_project.yaml
Normal file
145
files/demo_test_new_project.yaml
Normal file
@@ -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:
|
||||
55
files/demo_test_upload.yaml
Normal file
55
files/demo_test_upload.yaml
Normal file
@@ -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:
|
||||
BIN
files/gitlinklogo2.png
Normal file
BIN
files/gitlinklogo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
BIN
files/gitlinklogo3.jpg
Normal file
BIN
files/gitlinklogo3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
175
run.py
175
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
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user