进行一系列优化,且删除所有demo用例仅保持正常的示例

This commit is contained in:
floraachy
2023-11-10 15:58:03 +08:00
parent e97fe7290f
commit 4943adf36b
53 changed files with 906 additions and 868 deletions

100
README.md
View File

@@ -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"]是一些公共参数,如报告标题,报告名称,测试者,测试部门。后续会显示在测试报告上。如果还有其他,可自行添加
2ENV_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)

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:40
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -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}")

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:42
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View 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}")

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:39
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -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

View File

@@ -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):

View File

@@ -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__':

View File

@@ -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)

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:41
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View 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: 请求参数类型可选值为paramsjsondata
:param payload: 请求数据对于不同请求类型可以为dictMultipartEncoder等
: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参数不能为空')

View File

@@ -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])

View File

@@ -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)

View File

@@ -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" \
"-------------Startjson_extractor--------------------\n"
f"提取表达式为: {expr} \n"
f"提取值为: {result}\n"
"=====================================================")
print("提取响应内容成功,提取表达式为: {} 提取值为 {}".format(expr, result))
except Exception as e:
logger.debug("\n======================================================\n" \
logger.trace("\n======================================================\n" \
"-------------Endjson_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" \
"-------------Startre_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" \
"-------------Endre_extract--------------------\n"
f"提取表达式为: {expr}\n" \
f"提取数据为: {obj}\n" \
f"错误信息为:{e}\n" \
"=====================================================")
print(f'未提取到内容,请检查表达式是否错误!提取表达式为:{expr} 提取数据为 {obj} 错误信息为:{e}')
result = None
return result

View File

@@ -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" \

View File

@@ -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: 请求参数类型可选值为paramsjsondata
:param payload: 请求数据对于不同请求类型可以为dictMultipartEncoder等
: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参数不能为空')

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:47
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:46
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:46
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Time : 2023/11/10 14:45
# @Author : floraachy
# @File : __init__.py
# @Software: PyCharm
# @Desc:

View File

@@ -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
# 用例数据

View File

@@ -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": ""
}
}

View File

@@ -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: 提供程序执行过程中的关键信息。

View File

@@ -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钩子函数处理---------------------------------------#

View File

@@ -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:

View 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:

View File

@@ -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}

View File

@@ -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:

View File

@@ -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:

View 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:

View File

@@ -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")

View File

@@ -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"]

View 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:

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
files/gitlinklogo3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

175
run.py
View File

@@ -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

View File

@@ -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()

View File

@@ -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)