Commit 10b4056d by 15629057652

第一次尝试提交

parents
Showing with 7099 additions and 0 deletions
File added
##ignore this file##
/venv/
/target/
/.idea/
/.settings/
/.vscode/
/bin/
/report/
!index.pyc
*.pyc
.classpath
.project
.settings
.idea
##filter databfile、sln file##
*.mdb
*.ldb
*.sln
##class file##
*.com
*.class
*.dll
*.exe
*.q
*.o
*.so
# compression file
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.via
*.tmp
*.err
*.log
*.iml
# OS generated files #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.factorypath
/.mvn/
/mvnw.cmd
/mvnw
\ No newline at end of file
## 框架介绍
本框架主要是基于 Python + pytest + allure + log + yaml + mysql + redis + 钉钉通知 + Jenkins 实现的接口自动化框架。
## 前言
框架主要使用 python 语言编写,结合 pytest 进行二次开发,用户仅需要在 yaml 文件中编写测试用例,
编写成功之后,会自动生成 pytest 的代码,零基础代码小白,也可以操作。
本框架支持多业务接口依赖,多进程执行,mysql 数据库断言和 接口响应断言,并且用例直接在yaml文件中维护,无需编写业务代码,
接口pytest框架生成allure报告,并且发送 企业微信通知/ 钉钉通知/ 邮箱通知/ 飞书通知,灵活配置。
## 实现功能
* 测试数据隔离, 实现数据驱动
* 支持多接口数据依赖: 如A接口需要同时依赖B、C接口的响应数据作为参数
* 数据库断言: 直接在测试用例中写入查询的sql即可断言,无需编写代码
* 动态多断言: 如接口需要同时校验响应数据和sql校验,支持多场景断言
* 自动生成用例代码: 测试人员在yaml文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用
* 代理录制: 支持代理录制,生成yaml格式的测试用例
* 统计接口的运行时长: 拓展功能,订制开关,可以决定是否需要使用
* 日志模块: 打印每个接口的日志信息,同样订制了开关,可以决定是否需要打印日志
* 钉钉、企业微信通知: 支持多种通知场景,执行成功之后,可选择发送钉钉、或者企业微信、邮箱通知
* 自定义拓展字段: 如用例中需要生成的随机数据,可直接调用
* 多线程执行
* 支持swagger接口文档转成yaml用例,节省用例编写时间
## 目录结构
├── common // 配置
│ ├── conf.yaml // 公共配置
│ ├── setting.py // 环境路径存放区域
├── data // 测试用例数据
├── File // 上传文件接口所需的文件存放区域
├── logs // 日志层
├── report // 测试报告层
├── test_case // 测试用例代码
├── utils // 工具类
│ └── assertion
│ └── assert_control.py // 断言
│ └── assert_type.py // 断言类型
│ └── cache_process // 缓存处理模块
│ └── cacheControl.py
│ └── redisControl.py
│ └── logUtils // 日志处理模块
│ └── logControl.py
│ └── logDecoratrol.py // 日志装饰器
│ └── runTimeDecoratrol.py // 统计用例执行时长装饰器
│ └── mysqlUtils // 数据库模块
│ └── get_sql_data.py
│ └── mysqlControl.py
│ └── noticUtils // 通知模块
│ └── dingtalkControl.py // 钉钉通知
│ └── feishuControl.py // 飞书通知
│ └── sendmailControl.py // 邮箱通知
│ └── weChatSendControl.py // 企业微信通知
│ └── otherUtils // 其他工具类
│ └── allureDate // allure封装
│ └── allure_report_data.py // allure报告数据清洗
│ └── allure_tools.py // allure 方法封装
│ └── error_case_excel.py // 收集allure异常用例,生成excel测试报告
│ └── localIpControl.py // 获取本地IP
│ └── threadControl.py // 定时器类
│ └── readFilesUtils // 文件操作
│ └── caseAutomaticControl.py // 自动生成测试代码
│ └── clean_files.py // 清理文件
│ └── excelControl.py // 读写excel
│ └── get_all_files_path.py // 获取所有文件路径
│ └── get_yaml_data_analysis.py // yaml用例数据清洗
│ └── regularControl.py // 正则
│ └── yamlControl.py // yaml文件读写
│ └── recordingUtils // 代理录制
│ └── mitmproxyContorl.py
│ └── requestsUtils
│ └── dependentCase.py // 数据依赖处理
│ └── requestControl.py // 请求封装
│ └── timeUtils
├── Readme.md // help
├── pytest.ini
├── run.py // 运行入口
## 依赖库
allure-pytest==2.9.45
allure-python-commons==2.9.45
atomicwrites==1.4.0
attrs==21.2.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.7
colorama==0.4.4
colorlog==6.6.0
cryptography==36.0.0
DingtalkChatbot==1.5.3
execnet==1.9.0
Faker==9.8.3
idna==3.3
iniconfig==1.1.1
jsonpath==0.82
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycparser==2.21
PyMySQL==1.0.2
pyOpenSSL==21.0.0
pyparsing==3.0.6
pytest==6.2.5
pytest-forked==1.3.0
pytest-xdist==2.4.0
python-dateutil==2.8.2
PyYAML==6.0
requests==2.26.0
six==1.16.0
text-unidecode==1.3
toml==0.10.2
urllib3==1.26.7
xlrd==2.0.1
xlutils==2.0.0
xlwt==1.3.0
## 安装教程
首先,执行本框架之后,需要搭建好 python、jdk、 allure环境
搭建python教程:[http://c.biancheng.net/view/4161.html](http://c.biancheng.net/view/4161.html)
搭建jdk环境:[https://www.cnblogs.com/zll-wyf/p/15095664.html](https://www.cnblogs.com/zll-wyf/p/15095664.html)
安装allure:[https://blog.csdn.net/m0_49225959/article/details/117194318](https://blog.csdn.net/m0_49225959/article/details/117194318)
如上环境如都搭建好,则安装本框架的所有第三方库依赖,执行如下命令
pip3 install -r requirements.txt
![img.png](Files/image/安装异常.png)
如果在安装过程中出现如下 Could not find a version 类似的异常, 不用担心,可能是因为你安装的python环境
版本和我不一致导致的,直接 pip install 库名称,不指定版本安装就可以了。
如上方截图说没有找到 asgiref==3.5.1,报错的意思是,没有找到3.5.1这个版本,那么直接控制台输入 pip3 install asgiref 进行安装即可
## 接口文档
这里非常感谢一位安卓的朋友,给我推荐了开源的接口文件,框架中会针对开源接口中的登录、个人信息、收藏(新增、查看、修改、删除)等功能,编写结果自动化案例
下方是接口文档地址,大家可以自行查看(因为开源的接口,里面有些逻辑性的功能,如修改被删除的网址接口并没有过多的做判断,
因此用例中只写了一些基础的场景,仅供大家参考。)
[https://wanandroid.com/blog/show/2](https://wanandroid.com/blog/show/2)
## 如何创建用例
### 创建用例步骤
1、在data文件夹下方创建相关的yaml用例
2、写完之后,需要执行 utils\readFilesUtils\caseAutomaticControl.py 这个文件,生成自动化代码
3、执行caseAutomaticControl.py文件之后,会发现,在test_case层新增该条用例的对应代码,可直接执行该用例调试
4、注意,如果生成对应的测试代码之后,期间有更改过yaml用例中的内容,需要重新生成代码,必现因为更改yaml用例之后导致运行失败
5、当所有接口都编写好之后,可以直接运行run.py主程序,执行所有自动化接口
下面我们来看一下,如何创建用例
### 用例中相关字段的介绍
![img.png](Files/image/case_data.png)
上方截图,就是一个用例中需要维护的相关字段,下面我会对每个字段的作用,做出解释。
![img.png](Files/image/case_detail.png)
### 如何发送get请求
上方了解了用例的数据结构之后,下面我们开始编写第一个get请求方式的接口。
首先,开始编写项目之后,我们在 conf.yaml 中配置项目的域名
![img.png](Files/image/conf.png)
域名配置好之后,我们来编写测试用例,在 data 文件下面,创建一个名称为
collect_tool_list.yaml 的用例文件,请求/lg/collect/usertools/json这个收藏网址列表接口,所有接口的详细信息,可以在接口文档中查看,下方不在做赘述
接口文档:[https://wanandroid.com/blog/show/2](https://wanandroid.com/blog/show/2)
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 收藏网址列表接口
collect_tool_list_01:
host: ${{host()}}
url: /lg/collect/usertools/json
method: GET
detail: 查看收藏网址列表接口
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: login_cookie
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
pageNum: 1
pageSize: 10
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
sql:
get请求我们 requestType 写的是 params ,这样发送请求时,我们会将请求参数拼接中url中,最终像服务端发送请求的地址格式会为:
如: ${{host()}}/lg/collect/usertools/json?pageNum=1&pageSize=10
### 如何发送post请求
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 收藏网址接口
collect_addtool_01:
host: ${{host()}}
url: /lg/collect/addtool/json
method: POST
detail: 新增收藏网址接口
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: login_cookie
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
name: 自动化生成收藏网址${{random_int()}}
link: https://gitee.com/yu_xiao_qi/pytest-auto-api2
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
sql:
这里post请求,我们需要请求的数据格式是json格式的,那么requestType 则填写为json格式。
包括 PUT/DELETE/HEAD 请求的数据格式都是一样的,唯一不同的就是需要配置 reuqestType,
如果需要请求的参数是json格式,则requestType我们就填写json,如果是url拼接的形式,我们就填写 params
### 如何测试上传文件接口
首先,我们将所有需要测试的文件,全部都放在 files 文件夹中
![img.png](Files/image/files.png)
requestType: file
# 是否执行,空或者 true 都会执行
is_run:
data:
file:
xxx: 排入水体名.png
在yaml文件中,我们需要注意两个地方,主要是用例中的requestType、和 filename 字段:
* requestType: 上传文件,我们需要更改成 file
* file: 上传文件中,新增一个file关键字,在下方传我们需要的数据
* file_name: 首先,这个xxx是我们公司接口定义的上传文件的参数,排入水体名.png 这个是我们放在Files这个文件夹下方的文件名称
程序在执行的时候,会判断如果你的requestType为 file的时候,则会去执行file下方的参数,然后取到文件名称直接去执行用例
### 上传文件接口,即需要上传文件,又需要上传其他参数
requestType: file
# 是否执行,空或者 true 都会执行
is_run:
data:
file:
file_name: 排入水体名.png
data:
is_upload: 0
params:
collect: false
上方的这个案例,请求参数即上传了文件,又上传了其他参数
* 1、file: 这里下方上传的是文件参数
* 2、data: 这个data下方是该接口,除了文件参数,还需要上传其他的参数,这个参数会以json的方式传给服务端(如果没有其他参数,可以不用写这个)
* 3、params: 这个是除了文件参数以外的,上传的其他参数,这个参数是拼接在url后方的
![img.png](Files/image/files_up.png)
为了方便大家理解,上方将该参数,以postman的形式上传
### 多业务逻辑,如何编写测试用例
多业务这一块,我们拿个简单的例子举例,比如登录场景,在登陆之前,我们需要先获取到验证码。
![img.png](Files/image/send_sms_code.png)
![img.png](Files/image/login.png)
首先,我们先创建一个 get_send_sms_code.yaml 的文件,编写一条发送验证码的用例
# 公共参数
case_common:
allureEpic: 盲盒APP
allureFeature: 登录模块
allureStory: 获取登录验证码
send_sms_code_01:
host: ${{host()}}
url: /mobile/sendSmsCode
method: POST
detail: 正常获取登录验证码
headers:
appId: '23132'
masterAppId: masterAppId
Content-Type: application/json;charset=UTF-8
# 请求的数据,是 params 还是 json、或者file
requestType: json
# 是否执行,空或者 true 都会执行
is_run:
data:
phoneNumber: "180****9278"
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
code:
jsonpath: $.code
type: ==
value: '00000'
AssertType:
success:
jsonpath: $.success
type: ==
value: true
AssertType:
sql:
编写好之后,我们在创建一个 login.yaml 文件
# 公共参数
case_common:
allureEpic: 盲盒APP
allureFeature: 登录模块
allureStory: 登录
login_02:
host: ${{host()}}
url: /login/phone
method: POST
detail: 登录输入错误的验证码
headers:
appId: '23132'
masterAppId: masterAppId
Content-Type: application/json;charset=UTF-8
# 请求的数据,是 params 还是 json、或者file
requestType: json
# 是否执行,空或者 true 都会执行
is_run:
data:
phoneNumber: 18014909278
code: $cache{login_02_v_code}
# 是否有依赖业务,为空或者false则表示没有
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: send_sms_code_01
dependent_data:
- dependent_type: response
jsonpath: $.code
set_cache: login_02_v_code
assert:
code:
jsonpath: $.code
type: ==
value: '00000'
AssertType:
sql:
其中处理多业务的核心区域,主要在这里:
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: send_sms_code_01
dependent_data:
- dependent_type: response
jsonpath: $.code
set_cache: login_02_v_code
首先,我们 dependence_case 需要设置成 True,并且在下面的 dependence_case_data 中设计相关依赖的数据。
* case_id:上方场景中,我们登录需要先获取验证码,因此依赖的case_id 就是发送短信验证码的 case_id :send_sms_code_01
* dependent_type:我们依赖的是获取短信验证码接口中的响应内容,因此这次填写的是 response, 同样也支持request、sql等方式
* jsonpath: 通过jsonpath 提取方式,提取到短信验证码中的验证码内容(jsonpath规格和jmeter中的json在线提取器的规则一致)
* set_cache:拿到验证码之后,这里我们可以自定义一个缓存名称 如: login_02_v_code,程序中会将你所提取到的验证码存入缓存中,
因此我们在这条用例的 data 中,有个code 的参数,值设置成 $cache{login_02_v_code},程序中会将我们 send_sms_code_01中的验证码给提取出来,
通过 $cache{login_02_v_code} 语法获取到。
* 注意,定义缓存名称,每个公司最好定义一个规范,比如 当前这条 case_id名称 + 缓存自定义名称,如 login_02_v_code, case_id 是唯一的,
这样可以避免不同用例之间缓存名称重复的问题,导致无法获取到对应的缓存数据
### 多业务逻辑,需要依赖同一个接口中的多个数据
dependence_case_data:
- case_id: send_sms_code_01
dependent_data:
# 提取接口响应的code码
- dependent_type: response
jsonpath: $.code
set_cache: v_code
# 提取接口响应的accessToken
- dependent_type: response
jsonpath: $.data.accessToken
# 替换请求头中的accessToken
set_cache: accessToken
如上方示例,可以添加多个 dependent_type
### 多业务逻辑,需要依赖不同接口的数据
假设我们需要获取 send_sms_code_01、get_code_01两个接口中的数据,用例格式如下
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: send_sms_code_01
dependent_data:
# 提取接口响应的code码
- dependent_type: response
jsonpath: $.code
set_cache: v_code
- case_id: get_code_01
dependent_data:
# 提取接口响应的code码
- dependent_type: response
jsonpath: $.code
set_cache: v_code2
### 请求参数为路径参数
collect_delete_tool_01:
host: ${{host()}}
url: /lg/collect/deletetool/json/$cache{collect_delete_tool_01_id}
method: POST
detail: 正常删除收藏网站
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: None
# 是否执行,空或者 true 都会执行
is_run:
data:
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: collect_addtool_01
dependent_data:
- dependent_type: response
jsonpath: $.data.id
set_cache: collect_delete_tool_01_id
以上方实例,我们的参数是在url中的,因此我们可以通过 dependence_case 获取到我们需要依赖的数据,
将本条用例需要用到的数据存入缓存,从而在 /lg/collect/deletetool/json/$cache{collect_delete_tool_01_id} 直接调用缓存数据即可
### 将当前用例的请求值或者响应值存入缓存中
有些小伙伴之前有反馈过,比如想要做数据库的断言,但是这个字段接口没有返回,我应该怎么去做校验呢?
程序中提供了current_request_set_cache这个关键字,可以将当前这条用例的请求数据 或者响应数据 给直接存入缓存中
如下案例所示:
current_request_set_cache:
# 1、response 从响应中提取内容 2、request从请求中提取内容
- type: response
jsonpath: $.data.data.[0].id
# 自定义的缓存名称
name: test_query_shop_brand_list_02_id
### 请求用例时参数需要从数据库中提取
![img.png](Files/image/sql_params.png)
如上图所示,用例中的 dependent_type 需要填写成 sqlData。
当你的依赖类型为 sqlData 数据库的数据时,那么下方就需要再加一个 setup_sql 的参数,下方填写需要用到的sql语句
注意case_id: 因为程序设计原因,通常情况下,我们关联的业务,会发送接口请求,但是如果我们依赖的是sql的话,
是不需要发送请求的,因此我们如果是从数据库中提取数据作为参数的话,我们case_id 需要写self ,方便程序中去做区分
ApplyVerifyCode_01:
host: ${{host}}
url: /api/v1/merchant/apply/verifyCode
method: GET
detail: 校验已经审核通过的供应商手机号码
headers:
Content-Type: application/json;charset=UTF-8
# 请求的数据,是 params 还是 json、或者file、data
requestType: params
# 是否执行,空或者 true 都会执行
is_run:
data:
mobile: 18811111111
authCode: 123456
name: $cache{username}
# 是否有依赖业务,为空或者false则表示没有
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: self
dependent_data:
- dependent_type: sqlData
jsonpath: $.username
set_cache: username
assert:
code:
jsonpath: $.code
type: ==
value: 200
AssertType:
applyId:
jsonpath: $.data[0].applyId
type: ==
value: $.applyId
AssertType: SQL
applyStatus:
jsonpath: $.data[0].applyStatus
type: ==
value: $.applyStatus
AssertType: SQL
sql:
- select a.apply_id as applyId, a.to_status as applyStatus, a.sub_biz_type as subBizType, a.operator_name as operatorName, a.operator_user_id as operatorUserId, b.apply_type as applyType from test_obp_midware.apply_operate_log as a inner join test_obp_midware.apply as b on a.apply_id = b.id where b.id = $json($.data[0].applyId)$ order by a.id desc limit 1;
setup_sql:
- SELECT * FROM test_obp_user.user_biz_info where user_id = '300000405'
### 用例中需要依赖登录的token,如何设计
首先,为了防止重复请求调用登录接口,pytest中的 conftest.py 提供了热加载机制,看上方截图中的代码,我们需要在 conftest.py 提前编写好登录的代码。
如上方代码所示,我们会先去读取login.yaml文件中的用例,然后执行获取到响应中的token,然后 编写 Cache('work_login_init').set_caches(token),将token写入缓存中,其中 work_login_init 是缓存名称。
编写好之后,我们会在 requestControl.py 文件中,读取缓存中的token,如果该条用例需要依赖token,则直接进行内容替换。
@pytest.fixture(scope="session", autouse=True)
def work_login_init():
"""
获取登录的cookie
:return:
"""
url = "https://www.wanandroid.com/user/login"
data = {
"username": 18800000001,
"password": 123456
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# 请求登录接口
res = requests.post(url=url, data=data, verify=True, headers=headers).json()
token = res['response']['token']
CacheHandler.update_cache(cache_name='work_login_init', value=token)
这里在编写用例的时候,token 填写我们所编写的缓存名称即可。
![img.png](Files/image/img.png)
### 用例中依赖cookie如何设计
![img.png](Files/image/cookie.png)
首先我们在conftest.py中编写获取cookie的方法
@pytest.fixture(scope="session", autouse=True)
def work_login_init():
"""
获取登录的cookie
:return:
"""
url = "https://www.wanandroid.com/user/login"
data = {
"username": 18800000001,
"password": 123456
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# 请求登录接口
res = requests.post(url=url, data=data, verify=True, headers=headers)
response_cookie = res.cookies
cookies = ''
for k, v in response_cookie.items():
_cookie = k + "=" + v + ";"
# 拿到登录的cookie内容,cookie拿到的是字典类型,转换成对应的格式
cookies += _cookie
# 将登录接口中的cookie写入缓存中,其中login_cookie是缓存名称
CacheHandler.update_cache(cache_name='login_cookie', value=cookies)
和token一样,我们如果用例的请求头中依赖cookie, cookie中的值,直接写我们存入缓存中的名称即可
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
### 用例中如何生成随机数据
比如我们有些特殊的场景,可能会涉及到一些定制化的数据,每次执行数据,需要按照指定规则随机生成。
![img.png](Files/image/randoms.png)
如上图所示,我们用例中的 reason 审核原因后方,需要展示审核的当前时间。那么我们首先需要封装一个获取当前时间的方法
![img.png](Files/image/regular.png)
那么我们就在 regularControl.py 文件中,编写 get_time 的方法。编写好之后,在用例中编写规则如下:
reason: 审核时间${{get_time()}}
使用 " ${{函数名称()}}" 的方法,程序调用时,会生成当前时间。在regularControl.py 文件中,我还封装了一些常用的随机数,
如随机生成男生姓名、女生姓名、身份证、邮箱、手机号码之类的,方便大家使用。
如,随机生成邮箱,我们在用例中编写的格式为 " ${{get_email()}} " 。
其他所需随机生成的数据,可在文件中自行添加。
### 自动化函数传递参数
首先同样和上方一样,创建一个随机生成的方法,改方法支持接收参数
@classmethod
def random_int(cls, min_num, max_num):
"""
随机生成指定范围的随机数
@param min_num: 最小数字
@param max_num: 最大数字
@return:
"""
num = random.randint(int(min_num), int(max_num))
return num
在用例中,假设我们需要获取一个 1-10之间的随机数,那么我们直接这样调用该数据即可
reason: {{random_int(1, 10)}}
### 断言http响应状态码
相信有些小伙伴在做接口测试的过程中,有部分接口是没有任何响应的,那么在没有响应数据的情况下
我们就只能通过 http的状态码去判断这条用例是否通过,我们可以这样写
assert:
status_code: 200
我们直接在assert下方添加一个 status_code 参数,状态码我们判断其为 200
### 用例中添加等待时间
程序中可以设定接口请求之后,等待时长,假设A接口依赖B接口的业务,A接口请求完时,我们需要让他等待几秒钟
再次请求B接口,这样的话,我们可以使用sleep关键字
sleep: 3
### 断言类型
下放截图中,是所有断言支持的类型
![img.png](Files/image/assert_type.png)
### 用例中如何进行接口断言和数据库断言
假设现在我需要测试一个报表统计的数据,该接口返回了任务的处理时长 和 处理数量。功能如下截图所示:
![img.png](Files/image/question_coun.png)
假设下方是我们拿到接口响应的数据内容:
{"code": 200, "times": 155.91, "counts": 9}
这个时候,我们需要判断该接口返回的数据是否正确,就需要编写sql,对响应内容进行校验。
![img.png](Files/image/sql.png)
因此我们编写了如上sql,查出对应的数据,那么用例中编写规则如下,下方我们分别断言了两个内容,一个是对接口的响应code码进行断言,一个是断言数据库中的数据。
assert:
code:
jsonpath: $.code
type: ==
value: 200
# 断言接口响应时,可以为空
AssertType:
do_time:
# jsonpath 拿到接口响应的数据
jsonpath: $.times
type: ==
# sql 查出来的数据,是字典类型的,因此这里是从字段中提取查看出来的字段
value: $.do_time
# 断言sql的时候,AssertType 的值需要填写成 SQL
AssertType: SQL
question_counts:
jsonpath: $.counts
type: ==
#
value: $.question_counts
# 断言sql的时候,AssertType 的值需要填写成 SQL
AssertType: SQL
sql:
- select * from test_goods where shop_id = 515
我们分别对用例的数据进行讲解,首先是响应断言, 编写规则如下
code:
# 通过jsonpath获取接口响应中的code {"code": 200, "times": 155.91, "counts": 9}
jsonpath: $.code
type: ==
value: 200
# 断言接口响应时,可以为空
AssertType:
下面是对sql进行断言
question_counts:
# 断言接口响应的问题上报数量counts {"code": 200, "times": 155.91, "counts": 9}
jsonpath: $.counts
type: ==
# 查询sql,我们数据库查到的数据是一个字段,数据是这样的:{question_counts: 13, do_time: 1482.70}, 这里我们通过 jsonpath获取question_counts
value: $.question_counts
# 断言sql的时候,AssertType 的值需要填写成 SQL
AssertType: SQL
sql:
- SELECT round( sum(( UNIX_TIMESTAMP( filing_time )- UNIX_TIMESTAMP( report_time )) / 60 ) / 60, 2 ) AS do_time, count( id ) AS question_counts FROM fl_report_info WHERE state IN ( 1, 3 )
有些细心的小伙伴会发现,我们的sql,是列表类型的。这样就意味这,我们的sql可以同时编写多条,这样会对不会编写多表联查的小伙伴比较友好,可以进行单表查询,获取我们需要的数据。
sql:
- select * from users;
- select * from goods;
### 使用teardown功能,做数据清洗
通常情况下,我们做自动化所有新增的数据,我们测试完成之后,都需要讲这些数据删除,程序中支持两种写法
一种是直接调用接口进行数据删除。另外一种是直接删除数据库中的数据,建议使用第一种,直接调用业务接口删除对应的数据
1、下面我们先来看看第一种删除方式,teardown的功能,因为需要兼容较多的场景,因此使用功能上相对也会比较复杂
需要小伙伴们一个一个去慢慢的理解。
下面为了方便大家对于teardown功能的理解,我会针对不同的场景进行举例:
* 假设现在我们有一个新增接口,写完之后,我们需要先调用查询接口获取到新增接口的ID,然后再进行删除
那么此时会设计到两个场景,首先执行新增接口ID,然后再拿到响应(这里有个逻辑上的先后关系,查询接口,是先发送请求,在提取数据)
获取到查询的ID之后,我们在执行删除,删除的话,我们是直接发送请求
那么针对这个场景,我们就需要有个关键字去做区分,什么场景下先发送请求,什么场景下后发送请求,下面我们来看一下案例,方便大家理解
teardown:
# 查看品牌审核列表,获取品牌的apply_id
- case_id: query_apply_list_01
# 注意这里我们是先发送请求,在拿到自己响应的内容,因此我们这个字段需要写param_prepare
param_prepare:
# 因为是获取自己的响应内容,我们dependent_type需要写成 self_response
- dependent_type: self_response
# 通过jsonpath的方法,获取query_apply_list_01这个接口的响应内容
jsonpath: $.data.data.[0].applyId
# 将内容存入缓存,这个是自定义的缓存名称
set_cache: test_brand_apply_initiate_apply_01_applyId
# 支持同时存多个数据,只会发送一次请求
- dependent_type: self_response
jsonpath: $.data.data.[0].brandName
set_cache: test_brand_apply_initiate_apply_01_brandName
# 删除
- case_id: delete_01
# 删除的话,我们是直接发送请求的,因此我们这里写 send_request
send_request:
# 我们上方已经拿到了ID,并且将ID存入缓存中,因此这里依赖数据的类型为cache,直接从缓存中提取
- dependent_type: cache
# 这个是缓存名称
cache_data: test_brand_apply_initiate_apply_01_applyId
# 通过relace_key 去替换 delete_01 中的 applyID参数
replace_key: $.data.applyId
* 那么有些小伙伴会在想,同样我们以上方的接口场景为例,有些小伙伴会说,我公司的新增的接口,有直接返回ID,不需要调用查询接口
程序中当然也支持这种场景,我们只需要这么编写
- case_id: process_apply_01
# 同样这么写 send_request
send_request:
# 这里我们从响应中获取
- dependent_type: response
# 通过jsonpath的方式,获取响应的内容
jsonpath: $.data.id
# 使用repalce_key进行替换
replace_key: $.data.applyId
* 程序中也支持从请求里面获取内容,编写规则如下
- case_id: process_apply_01
# 同样这么写 send_request
send_request:
# 这里我们从响应中获取
- dependent_type: request
# 通过jsonpath的方式,获取请求的内容
jsonpath: $.data.id
# 使用repalce_key进行替换
replace_key: $.data.applyId
### 使用 teardown_sql 后置sql删除数据
如一些特殊场景,业务上并没有提供删除接口,我们也可以直接通过 sql去讲对应的sql删除
teardown_sql:
- delete * from xxx
- delete * from xxx
### 自动生成test_case层代码
小伙伴们在编写好 yaml 用例之后,可以直接执行 caseAutomaticControl.py ,会跟你设计的测试用例,生成对应的代码。
![img.png](Files/image/write_test_case.png)
### 发送钉钉通知通知
![img.png](Files/image/dingding.png)
### 发送企业微信通知
![img.png](Files/image/wechart.png)
### 日志打印装饰器
![img.png](Files/image/log.png)
在requestControl.py中,我单独封装了一个日志装饰器,需要的小伙伴可以不用改动代码,直接使用,如果不需要,直接注释,或者改成False。控制台将不会有日志输出
### 统计用例运行时长
![img.png](Files/image/run_times.png)
同样,这里封装了一个统计用例运行时长的装饰器,使用改装饰器前,需要先进行导包
from utils.logUtils.runTimeDecoratorl import execution_duration
导入之后,调用改装饰器,装饰器中填写的用例执行时长,以毫秒为单位,如这里设置的2000ms,那么如果该用例执行大于2000ms,则会输出一条告警日志。
@execution_duration(2000)
### 生成allure报告
我们直接运行主程序 run.py ,运行完成之后,就可以生成漂亮的allure报告啦~
![img.png](Files/image/allure.png)
project_name: xxx项目名称
env: 测试环境
# 测试人员名称,作用于自动生成代码的作者,以及发送企业微信、钉钉通知的测试负责人
tester_name: lyd
# 域名1
host: https://www.wanandroid.com
# 域名2,支持多个域名配置
app_host:
# 实时更新用例内容,False时,已生成的代码不会在做变更
# 设置为True的时候,修改yaml文件的用例,代码中的内容会实时更新
real_time_update_test_cases: False
# 报告通知类型:0: 不发送通知 1:钉钉 2:企业微信通知 3、邮箱通知 4、飞书通知
# 支持同时发送多个通知,如多个,则用逗号分割, 如 1, 2
notification_type: 0
# 收集失败的用例开关,整理成excel报告的形式,自动发送,目前只支持返送企业微信通知
excel_report: False
# 钉钉相关配置
ding_talk:
webhook: https://oapi.dingtalk.com/robot/send?access_token=5052516dc545509a2674d67bf0092f4a803b151ce165748013d3f30388cf3e81
secret: SECc9c961257fbab55744f3163f6de14676699792ebe54e6ce96696517d1a847625
# webhook:
# secret:
# 数据库相关配置
mysql_db:
# 数据库开关
switch: False
host:
user: root
password: '123456'
port: 3306
# 镜像源
mirror_source: http://mirrors.aliyun.com/pypi/simple/
# 企业通知的相关配置
wechat:
webhook:
### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
### 重要的事情说三遍
email:
send_user:
email_host:
# 自己到QQ邮箱中配置stamp_key
stamp_key:
# 收件人改成自己的邮箱
send_list:
# 飞书通知
lark:
webhook:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from typing import Text
def root_path():
""" 获取 根路径 """
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return path
def ensure_path_sep(path: Text) -> Text:
"""兼容 windows 和 linux 不同环境的操作系统路径 """
if "/" in path:
path = os.sep.join(path.split("/"))
if "\\" in path:
path = os.sep.join(path.split("\\"))
return root_path() + path
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 收藏网址接口
collect_addtool_01:
host: ${{host()}}
url: /lg/collect/addtool/json
method: POST
detail: 新增收藏网址接口
headers:
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
name: 自动化
link: https://gitee.com/yu_xiao_qi/pytest-api
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
message: "errorCode 断言为 0"
current_request_set_cache:
- type: response
jsonpath: $.data.id
# 自定义的缓存名称
name: yushaoqi_sql
sql:
teardown:
- case_id: collect_delete_tool_01
send_request:
- dependent_type: response
jsonpath: $.data.id
replace_key: $.data.id
teardown_sql:
- UPDATE `api_test`.`ysq_test` SET `name` = '$json($.data.id)$' WHERE `name` = '2' LIMIT 1
collect_addtool_02:
host: ${{host()}}
url: /lg/collect/addtool/json
method: POST
detail: 未登录状态下新增收藏网址
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
name: 自动生成收藏网址${{random_int()}}
link: https://gitee.com/yu_xiao_qi/pytest-api
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
status_code: 200
# 断言接口状态码
errorCode:
# 断言接口状态码
jsonpath: $.errorCode
type: ==
value: -1001
AssertType:
errorMsg:
jsonpath: $.errorMsg
type: ==
value: '请先登录!'
AssertType:
sql:
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 删除收藏网站接口
collect_delete_tool_01:
host: ${{host()}}
url: /lg/collect/deletetool/json
method: POST
detail: 正常删除收藏网站
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
id: $cache{collect_delete_tool_01_id}
id2: 2
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: collect_addtool_01
dependent_data:
- dependent_type: response
jsonpath: $.data.id
set_cache: collect_delete_tool_01_id
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
sql:
collect_delete_tool_02:
host: ${{host()}}
url: /lg/collect/deletetool/json
method: POST
detail: 正常删除不存在的ID数据(接口未完成此功能,跳过该条用例)
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run: False
data:
id: 111
# 是否有依赖业务,为空或者false则表示没有
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: collect_addtool_01
dependent_data:
- dependent_type: response
jsonpath: $.data.id
replace_key: $.data.id
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
sql:
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 收藏网址列表接口
collect_tool_list_01:
host: ${{host()}}
url: /lg/collect/usertools/json
method: GET
detail: 查看收藏网址列表接口
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: None
# 是否执行,空或者 true 都会执行
is_run:
data:
# 是否有依赖业务,为空或者false则表示没有
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: self
dependent_data:
- dependent_type: sqlData
jsonpath: $.business_type
set_cache: yushaoqi
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
status_code: 200
sql:
setup_sql:
- SELECT * FROM `api_test`.`t_open_field_cfg_copy1` LIMIT 0,1;
sleep: 2
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 收藏模块
allureStory: 编辑收藏网址接口
collect_update_tool_01:
host: ${{host()}}
url: /lg/collect/addtool/json
method: POST
detail: 编辑收藏网址
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run: False
data:
name: 自动化编辑网址名称
link: https://gitee.com/yu_xiao_qi/pytest-api
id:
# 是否有依赖业务,为空或者false则表示没有
dependence_case: True
# 依赖的数据
dependence_case_data:
- case_id: collect_addtool_01
dependent_data:
- dependent_type: response
jsonpath: $.data.id
replace_key: $.data.id
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
sql:
teardown:
# 先搜索
- case_id: collect_tool_list_01
param_prepare:
- dependent_type: self_response
jsonpath: $.data[-1:].id
set_cache: $set_cache{artile_id}
# 删除
- case_id: collect_delete_tool_01
send_request:
# 删除从缓存中拿数据
- dependent_type: cache
cache_data: int:artile_id
replace_key: $.data.id
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 登录模块
allureStory: 登录
login_01:
host: ${{host()}}
url: /user/login
method: POST
detail: 正常登录
headers:
# Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '18800000001'
password: '123456'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
# 断言接口返回的username
username:
jsonpath: $.data.username
type: ==
value: '18800000001'
AssertType:
sql:
login_02:
host: ${{host()}}
url: /user/login
method: POST
detail: 输入错误的密码
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '18800000001'
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_03:
host: ${{host()}}
url: /user/login
method: POST
detail: 登录密码为空
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '18800000001'
password:
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_04:
host: ${{host()}}
url: /user/login
method: POST
detail: 输入非1开头的手机号码
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '28800000001'
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_05:
host: ${{host()}}
url: /user/login
method: POST
detail: 输入手机号码小于11位
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '1880000000'
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_06:
host: ${{host()}}
url: /user/login
method: POST
detail: 输入手机号码大于于11位
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: '18800000000'
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_07:
host: ${{host()}}
url: /user/login
method: POST
detail: 手机号码为空
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username:
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
login_08:
host: ${{host()}}
url: /user/login
method: POST
detail: 手机号码首位包含空格
headers:
Content-Type: multipart/form-data;
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
username: ' 18867507063 '
password: '12345'
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: -1
AssertType:
# 断言接口返回的username
errorMsg:
jsonpath: $.errorMsg
type: ==
value: "账号密码不匹配!"
AssertType:
sql:
# 公共参数
case_common:
allureEpic: 开发平台接口
allureFeature: 个人信息模块
allureStory: 个人信息接口
get_user_info_01:
host: ${{host()}}
url: /user/lg/userinfo/json
method: GET
detail: 正常获取个人身份信息
headers:
Content-Type: multipart/form-data;
# 这里cookie的值,写的是存入缓存的名称
cookie: $cache{login_cookie}
# 请求的数据,是 params 还是 json、或者file、data
requestType: data
# 是否执行,空或者 true 都会执行
is_run:
data:
# 是否有依赖业务,为空或者false则表示没有
dependence_case: False
# 依赖的数据
dependence_case_data:
assert:
# 断言接口状态码
errorCode:
jsonpath: $.errorCode
type: ==
value: 0
AssertType:
# 断言接口返回的username
username:
jsonpath: $.data.userInfo.username
type: ==
value: '18800000001'
AssertType:
sql:
[pytest]
addopts = -p no:warnings
testpaths = test_case/
python_files = test_*.py
python_classes = Test*
python_function = test_*
markers =
smoke: 冒烟测试
aiofiles==0.8.0
allure-pytest==2.9.45
allure-python-commons==2.9.45
asgiref==3.5.1
atomicwrites==1.4.0
attrs==21.2.0
blinker==1.4
Brotli==1.0.9
certifi==2021.10.8
cffi==1.15.0
chardet==4.0.0
charset-normalizer==2.0.7
click==8.1.3
colorama==0.4.4
colorlog==6.6.0
cryptography==36.0.0
DingtalkChatbot==1.5.3
et-xmlfile==1.1.0
execnet==1.9.0
Faker==9.8.3
Flask==2.0.3
h11==0.13.0
h2==4.1.0
hpack==4.0.0
httptools==0.4.0
hyperframe==6.0.1
idna==3.3
iniconfig==1.1.0
itchat==1.3.10
itsdangerous==2.1.2
Jinja2==3.1.2
jsonpath==0.82
kaitaistruct==0.9
ldap3==2.9.1
MarkupSafe==2.1.1
mitmproxy~=8.1.0
msgpack==1.0.3
multidict==6.0.2
openpyxl==3.0.9
packaging==21.3
passlib==1.7.4
pluggy==1.0.0
protobuf==3.19.4
publicsuffix2==2.20191221
py==1.11.0
pyasn1==0.4.8
pycparser==2.21
pydivert==2.1.0
PyMySQL==1.0.2
pyOpenSSL==21.0.0
pyparsing==3.0.6
pyperclip==1.8.2
pypng==0.0.21
PyQRCode==1.2.1
pytest~=7.1.2
pytest-forked==1.3.0
pytest-xdist==2.4.0
python-dateutil==2.8.2
pywin32==304
PyYAML~=5.4.1
requests==2.26.0
requests-toolbelt==0.9.1
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.6
sanic==22.3.1
sanic-routing==22.3.0
six==1.16.0
sortedcontainers==2.4.0
text-unidecode==1.3
toml==0.10.2
tornado==6.1
urllib3==1.26.7
urwid==2.1.2
websockets==10.3
Werkzeug==2.1.2
wsproto==1.1.0
xlrd==2.0.1
xlutils==2.0.0
xlwings==0.27.7
xlwt==1.3.0
zstandard==0.17.0
pyDes~=2.0.1
crypto~=1.4.1
redis~=4.3.4
pydantic~=1.8.2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import traceback
import pytest
from utils.other_tools.models import NotificationType
from utils.other_tools.allure_data.allure_report_data import AllureFileClean
from utils.logging_tool.log_control import INFO
from utils.notify.wechat_send import WeChatSend
from utils.notify.ding_talk import DingTalkSendMsg
from utils.notify.send_mail import SendEmail
from utils.notify.lark import FeiShuTalkChatBot
from utils.other_tools.allure_data.error_case_excel import ErrorCaseExcel
from utils.other_tools.allure_data.error_case_excel import ErrorCaseExcel
from utils import config
def run():
# 从配置文件中获取项目名称
try:
INFO.logger.info(
"""
_ _ _ _____ _
__ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_
/ _` | '_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __|
| (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_
\\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__|
|_|
开始执行{}项目...
""".format(config.project_name)
)
# 判断现有的测试用例,如果未生成测试代码,则自动生成
# TestCaseAutomaticGeneration().get_case_automatic()
pytest.main(['-s', '-W', 'ignore:Module already imported:pytest.PytestWarning',
'--alluredir', './report/tmp', "--clean-alluredir"])
"""
--reruns: 失败重跑次数
--count: 重复执行次数
-v: 显示错误位置以及错误的详细信息
-s: 等价于 pytest --capture=no 可以捕获print函数的输出
-q: 简化输出信息
-m: 运行指定标签的测试用例
-x: 一旦错误,则停止运行
--maxfail: 设置最大失败次数,当超出这个阈值时,则不会在执行测试用例
"--reruns=3", "--reruns-delay=2"
"""
os.system(r"allure generate ./report/tmp -o ./report/html --clean")
allure_data = AllureFileClean().get_case_count()
notification_mapping = {
NotificationType.DING_TALK.value: DingTalkSendMsg(allure_data).send_ding_notification,
NotificationType.WECHAT.value: WeChatSend(allure_data).send_wechat_notification,
NotificationType.EMAIL.value: SendEmail(allure_data).send_main,
NotificationType.FEI_SHU.value: FeiShuTalkChatBot(allure_data).post
}
if config.notification_type != NotificationType.DEFAULT.value:
notify_type = config.notification_type.split(",")
for i in notify_type:
notification_mapping.get(i.lstrip(""))()
if config.excel_report:
ErrorCaseExcel().write_case()
# 程序运行之后,自动启动报告,如果不想启动报告,可注释这段代码
os.system(f"allure serve ./report/tmp -h 127.0.0.1 -p 9999")
except Exception:
# 如有异常,相关异常发送邮件
e = traceback.format_exc()
send_email = SendEmail(AllureFileClean.get_case_count())
send_email.error_mail(e)
raise
if __name__ == '__main__':
run()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['collect_addtool_01', 'collect_addtool_02']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("收藏模块")
class TestCollectAddtool:
@allure.story("收藏网址接口")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_collect_addtool(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_collect_addtool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['collect_delete_tool_01', 'collect_delete_tool_02']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("收藏模块")
class TestCollectDeleteTool:
@allure.story("删除收藏网站接口")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_collect_delete_tool(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_collect_delete_tool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['collect_tool_list_01']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("收藏模块")
class TestCollectToolList:
@allure.story("收藏网址列表接口")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_collect_tool_list(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_collect_tool_list.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['collect_update_tool_01']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("收藏模块")
class TestCollectUpdateTool:
@allure.story("编辑收藏网址接口")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_collect_update_tool(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_collect_update_tool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['login_01', 'login_02', 'login_03', 'login_04', 'login_05', 'login_06', 'login_07', 'login_08']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("登录模块")
class TestLogin:
@allure.story("登录")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_login(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_login.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023-03-20 13:55:04
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = ['get_user_info_01']
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("开发平台接口")
@allure.feature("个人信息模块")
class TestGetUserInfo:
@allure.story("个人信息接口")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_get_user_info(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['test_test_get_user_info.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from common.setting import ensure_path_sep
from utils.read_files_tools.get_yaml_data_analysis import CaseData
from utils.read_files_tools.get_all_files_path import get_all_files
from utils.cache_process.cache_control import CacheHandler, _cache_config
def write_case_process():
"""
获取所有用例,写入用例池中
:return:
"""
# 循环拿到所有存放用例的文件路径
for i in get_all_files(file_path=ensure_path_sep("\\data"), yaml_data_switch=True):
# 循环读取文件中的数据
case_process = CaseData(i).case_process(case_id_switch=True)
if case_process is not None:
# 转换数据类型
for case in case_process:
for k, v in case.items():
# 判断 case_id 是否已存在
case_id_exit = k in _cache_config.keys()
# 如果case_id 不存在,则将用例写入缓存池中
if case_id_exit is False:
CacheHandler.update_cache(cache_name=k, value=v)
# case_data[k] = v
# 当 case_id 为 True 存在时,则跑出异常
elif case_id_exit is True:
raise ValueError(f"case_id: {k} 存在重复项, 请修改case_id\n"
f"文件路径: {i}")
write_case_process()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest
import time
import allure
import requests
import ast
from common.setting import ensure_path_sep
from utils.requests_tool.request_control import cache_regular
from utils.logging_tool.log_control import INFO, ERROR, WARNING
from utils.other_tools.models import TestCase
from utils.read_files_tools.clean_files import del_file
from utils.other_tools.allure_data.allure_tools import allure_step, allure_step_no
from utils.cache_process.cache_control import CacheHandler
@pytest.fixture(scope="session", autouse=False)
def clear_report():
"""如clean命名无法删除报告,这里手动删除"""
del_file(ensure_path_sep("\\report"))
@pytest.fixture(scope="session", autouse=True)
def work_login_init():
"""
获取登录的cookie
:return:
"""
url = "https://www.wanandroid.com/user/login"
data = {
"username": 18800000001,
"password": 123456
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# 请求登录接口
res = requests.post(url=url, data=data, verify=True, headers=headers)
response_cookie = res.cookies
cookies = ''
for k, v in response_cookie.items():
_cookie = k + "=" + v + ";"
# 拿到登录的cookie内容,cookie拿到的是字典类型,转换成对应的格式
cookies += _cookie
# 将登录接口中的cookie写入缓存中,其中login_cookie是缓存名称
CacheHandler.update_cache(cache_name='login_cookie', value=cookies)
def pytest_collection_modifyitems(items):
"""
测试用例收集完成时,将收集到的 item 的 name 和 node_id 的中文显示在控制台上
:return:
"""
for item in items:
item.name = item.name.encode("utf-8").decode("unicode_escape")
item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
# 期望用例顺序
# print("收集到的测试用例:%s" % items)
appoint_items = ["test_get_user_info", "test_collect_addtool", "test_Cart_List", "test_ADD", "test_Guest_ADD",
"test_Clear_Cart_Item"]
# 指定运行顺序
run_items = []
for i in appoint_items:
for item in items:
module_item = item.name.split("[")[0]
if i == module_item:
run_items.append(item)
for i in run_items:
run_index = run_items.index(i)
items_index = items.index(i)
if run_index != items_index:
n_data = items[run_index]
run_index = items.index(n_data)
items[items_index], items[run_index] = items[run_index], items[items_index]
def pytest_configure(config):
config.addinivalue_line("markers", 'smoke')
config.addinivalue_line("markers", '回归测试')
@pytest.fixture(scope="function", autouse=True)
def case_skip(in_data):
"""处理跳过用例"""
in_data = TestCase(**in_data)
if ast.literal_eval(cache_regular(str(in_data.is_run))) is False:
allure.dynamic.title(in_data.detail)
allure_step_no(f"请求URL: {in_data.is_run}")
allure_step_no(f"请求方式: {in_data.method}")
allure_step("请求头: ", in_data.headers)
allure_step("请求数据: ", in_data.data)
allure_step("依赖数据: ", in_data.dependence_case_data)
allure_step("预期数据: ", in_data.assert_data)
pytest.skip()
def pytest_terminal_summary(terminalreporter):
"""
收集测试结果
"""
_PASSED = len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown'])
_ERROR = len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown'])
_FAILED = len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown'])
_SKIPPED = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown'])
_TOTAL = terminalreporter._numcollected
_TIMES = time.time() - terminalreporter._sessionstarttime
INFO.logger.error(f"用例总数: {_TOTAL}")
INFO.logger.error(f"异常用例数: {_ERROR}")
ERROR.logger.error(f"失败用例数: {_FAILED}")
WARNING.logger.warning(f"跳过用例数: {_SKIPPED}")
INFO.logger.info("用例执行时长: %.2f" % _TIMES + " s")
try:
_RATE = _PASSED / _TOTAL * 100
INFO.logger.info("用例成功率: %.2f" % _RATE + " %")
except ZeroDivisionError:
INFO.logger.info("用例成功率: 0.00 %")
from utils.read_files_tools.yaml_control import GetYamlData
from common.setting import ensure_path_sep
from utils.other_tools.models import Config
_data = GetYamlData(ensure_path_sep("\\common\\config.yaml")).get_yaml_data()
config = Config(**_data)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
断言类型封装,支持json响应断言、数据库断言
"""
import ast
import json
from typing import Text, Dict, Any, Union
from jsonpath import jsonpath
from utils.other_tools.models import AssertMethod
from utils.logging_tool.log_control import ERROR, WARNING
from utils.read_files_tools.regular_control import cache_regular
from utils.other_tools.models import load_module_functions
from utils.assertion import assert_type
from utils.other_tools.exceptions import JsonpathExtractionFailed, SqlNotFound, AssertTypeError
from utils import config
class AssertUtil:
def __init__(self, assert_data, sql_data, request_data, response_data, status_code):
self.response_data = response_data
self.request_data = request_data
self.sql_data = sql_data
self.assert_data = assert_data
self.sql_switch = config.mysql_db.switch
self.status_code = status_code
@staticmethod
def literal_eval(attr):
return ast.literal_eval(cache_regular(str(attr)))
@property
def get_assert_data(self):
assert self.assert_data is not None, (
"'%s' should either include a `assert_data` attribute, "
% self.__class__.__name__
)
return ast.literal_eval(cache_regular(str(self.assert_data)))
@property
def get_type(self):
assert 'type' in self.get_assert_data.keys(), (
" 断言数据: '%s' 中缺少 `type` 属性 " % self.get_assert_data
)
# 获取断言类型对应的枚举值
name = AssertMethod(self.get_assert_data.get("type")).name
return name
@property
def get_value(self):
assert 'value' in self.get_assert_data.keys(), (
" 断言数据: '%s' 中缺少 `value` 属性 " % self.get_assert_data
)
return self.get_assert_data.get("value")
@property
def get_jsonpath(self):
assert 'jsonpath' in self.get_assert_data.keys(), (
" 断言数据: '%s' 中缺少 `jsonpath` 属性 " % self.get_assert_data
)
return self.get_assert_data.get("jsonpath")
@property
def get_assert_type(self):
assert 'AssertType' in self.get_assert_data.keys(), (
" 断言数据: '%s' 中缺少 `AssertType` 属性 " % self.get_assert_data
)
return self.get_assert_data.get("AssertType")
@property
def get_message(self):
"""
获取断言描述,如果未填写,则返回 `None`
:return:
"""
return self.get_assert_data.get("message", None)
@property
def get_sql_data(self):
# 判断数据库开关为开启,并需要数据库断言的情况下,未编写sql,则抛异常
if self.sql_switch_handle:
assert self.sql_data != {'sql': None}, (
"请在用例中添加您要查询的SQL语句。"
)
# 处理 mysql查询出来的数据类型如果是bytes类型,转换成str类型
if isinstance(self.sql_data, bytes):
return self.sql_data.decode('utf=8')
sql_data = jsonpath(self.sql_data, self.get_value)
assert sql_data is not False, (
f"数据库断言数据提取失败,提取对象: {self.sql_data} , 当前语法: {self.get_value}"
)
if len(sql_data) > 1:
return sql_data
return sql_data[0]
@staticmethod
def functions_mapping():
return load_module_functions(assert_type)
@property
def get_response_data(self):
return json.loads(self.response_data)
@property
def sql_switch_handle(self):
"""
判断数据库开关,如果未开启,则打印断言部分的数据
:return:
"""
if self.sql_switch is False:
WARNING.logger.warning(
"检测到数据库状态为关闭状态,程序已为您跳过此断言,断言值:%s" % self.get_assert_data
)
return self.sql_switch
def _assert(self, check_value: Any, expect_value: Any, message: Text = ""):
self.functions_mapping()[self.get_type](check_value, expect_value, str(message))
@property
def _assert_resp_data(self):
resp_data = jsonpath(self.get_response_data, self.get_jsonpath)
assert resp_data is not False, (
f"jsonpath数据提取失败,提取对象: {self.get_response_data} , 当前语法: {self.get_jsonpath}"
)
if len(resp_data) > 1:
return resp_data
return resp_data[0]
@property
def _assert_request_data(self):
req_data = jsonpath(self.request_data, self.get_jsonpath)
assert req_data is not False, (
f"jsonpath数据提取失败,提取对象: {self.request_data} , 当前语法: {self.get_jsonpath}"
)
if len(req_data) > 1:
return req_data
return req_data[0]
def assert_type_handle(self):
# 判断请求参数数据库断言
if self.get_assert_type == "R_SQL":
self._assert(self._assert_request_data, self.get_sql_data, self.get_message)
# 判断请求参数为响应数据库断言
elif self.get_assert_type == "SQL" or self.get_assert_type == "D_SQL":
self._assert(self._assert_resp_data, self.get_sql_data, self.get_message)
# 判断非数据库断言类型
elif self.get_assert_type is None:
self._assert(self._assert_resp_data, self.get_value, self.get_message)
else:
raise AssertTypeError("断言失败,目前只支持数据库断言和响应断言")
class Assert(AssertUtil):
def assert_data_list(self):
assert_list = []
for k, v in self.assert_data.items():
if k == "status_code":
assert self.status_code == v, "响应状态码断言失败"
else:
assert_list.append(v)
return assert_list
def assert_type_handle(self):
for i in self.assert_data_list():
self.assert_data = i
super().assert_type_handle()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Assert 断言类型
"""
from typing import Any, Union, Text
def equals(
check_value: Any, expect_value: Any, message: Text = ""
):
"""判断是否相等"""
assert check_value == expect_value, message
def less_than(
check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
):
"""判断实际结果小于预期结果"""
assert check_value < expect_value, message
def less_than_or_equals(
check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""):
"""判断实际结果小于等于预期结果"""
assert check_value <= expect_value, message
def greater_than(
check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
):
"""判断实际结果大于预期结果"""
assert check_value > expect_value, message
def greater_than_or_equals(
check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
):
"""判断实际结果大于等于预期结果"""
assert check_value >= expect_value, message
def not_equals(
check_value: Any, expect_value: Any, message: Text = ""
):
"""判断实际结果不等于预期结果"""
assert check_value != expect_value, message
def string_equals(
check_value: Text, expect_value: Any, message: Text = ""
):
"""判断字符串是否相等"""
assert check_value == expect_value, message
def length_equals(
check_value: Text, expect_value: int, message: Text = ""
):
"""判断长度是否相等"""
assert isinstance(
expect_value, int
), "expect_value 需要为 int 类型"
assert len(check_value) == expect_value, message
def length_greater_than(
check_value: Text, expect_value: Union[int, float], message: Text = ""
):
"""判断长度大于"""
assert isinstance(
expect_value, (float, int)
), "expect_value 需要为 float/int 类型"
assert len(str(check_value)) > expect_value, message
def length_greater_than_or_equals(
check_value: Text, expect_value: Union[int, float], message: Text = ""
):
"""判断长度大于等于"""
assert isinstance(
expect_value, (int, float)
), "expect_value 需要为 float/int 类型"
assert len(check_value) >= expect_value, message
def length_less_than(
check_value: Text, expect_value: Union[int, float], message: Text = ""
):
"""判断长度小于"""
assert isinstance(
expect_value, (int, float)
), "expect_value 需要为 float/int 类型"
assert len(check_value) < expect_value, message
def length_less_than_or_equals(
check_value: Text, expect_value: Union[int, float], message: Text = ""
):
"""判断长度小于等于"""
assert isinstance(
expect_value, (int, float)
), "expect_value 需要为 float/int 类型"
assert len(check_value) <= expect_value, message
def contains(check_value: Any, expect_value: Any, message: Text = ""):
"""判断期望结果内容包含在实际结果中"""
assert isinstance(
check_value, (list, tuple, dict, str, bytes)
), "expect_value 需要为 list/tuple/dict/str/bytes 类型"
assert expect_value in check_value, message
def contained_by(check_value: Any, expect_value: Any, message: Text = ""):
"""判断实际结果包含在期望结果中"""
assert isinstance(
expect_value, (list, tuple, dict, str, bytes)
), "expect_value 需要为 list/tuple/dict/str/bytes 类型"
assert check_value in expect_value, message
def startswith(
check_value: Any, expect_value: Any, message: Text = ""
):
"""检查响应内容的开头是否和预期结果内容的开头相等"""
assert str(check_value).startswith(str(expect_value)), message
def endswith(
check_value: Any, expect_value: Any, message: Text = ""
):
"""检查响应内容的结尾是否和预期结果内容相等"""
assert str(check_value).endswith(str(expect_value)), message
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
缓存文件处理
"""
import os
from typing import Any, Text, Union
from common.setting import ensure_path_sep
from utils.other_tools.exceptions import ValueNotFoundError
class Cache:
""" 设置、读取缓存 """
def __init__(self, filename: Union[Text, None]) -> None:
# 如果filename不为空,则操作指定文件内容
if filename:
self.path = ensure_path_sep("\\cache" + filename)
# 如果filename为None,则操作所有文件内容
else:
self.path = ensure_path_sep("\\cache")
def set_cache(self, key: Text, value: Any) -> None:
"""
设置缓存, 只支持设置单字典类型缓存数据, 缓存文件如以存在,则替换之前的缓存内容
:return:
"""
with open(self.path, 'w', encoding='utf-8') as file:
file.write(str({key: value}))
def set_caches(self, value: Any) -> None:
"""
设置多组缓存数据
:param value: 缓存内容
:return:
"""
with open(self.path, 'w', encoding='utf-8') as file:
file.write(str(value))
def get_cache(self) -> Any:
"""
获取缓存数据
:return:
"""
try:
with open(self.path, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
pass
def clean_cache(self) -> None:
"""删除所有缓存文件"""
if not os.path.exists(self.path):
raise FileNotFoundError(f"您要删除的缓存文件不存在 {self.path}")
os.remove(self.path)
@classmethod
def clean_all_cache(cls) -> None:
"""
清除所有缓存文件
:return:
"""
cache_path = ensure_path_sep("\\cache")
# 列出目录下所有文件,生成一个list
list_dir = os.listdir(cache_path)
for i in list_dir:
# 循环删除文件夹下得所有内容
os.remove(cache_path + i)
_cache_config = {}
class CacheHandler:
@staticmethod
def get_cache(cache_data):
try:
return _cache_config[cache_data]
except KeyError:
raise ValueNotFoundError(f"{cache_data}的缓存数据未找到,请检查是否将该数据存入缓存中")
@staticmethod
def update_cache(*, cache_name, value):
_cache_config[cache_name] = value
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
redis 缓存操作封装
"""
from typing import Text, Any
import redis
class RedisHandler:
""" redis 缓存读取封装 """
def __init__(self):
self.host = '127.0.0.0'
self.port = 6379
self.database = 0
self.password = 123456
self.charset = 'UTF-8'
self.redis = redis.Redis(
self.host,
port=self.port,
password=self.password,
decode_responses=True,
db=self.database
)
def set_string(
self, name: Text,
value, exp_time=None,
exp_milliseconds=None,
name_not_exist=False,
name_exit=False) -> None:
"""
缓存中写入 str(单个)
:param name: 缓存名称
:param value: 缓存值
:param exp_time: 过期时间(秒)
:param exp_milliseconds: 过期时间(毫秒)
:param name_not_exist: 如果设置为True,则只有name不存在时,当前set操作才执行(新增)
:param name_exit: 如果设置为True,则只有name存在时,当前set操作才执行(修改)
:return:
"""
self.redis.set(
name,
value,
ex=exp_time,
px=exp_milliseconds,
nx=name_not_exist,
xx=name_exit
)
def key_exit(self, key: Text):
"""
判断redis中的key是否存在
:param key:
:return:
"""
return self.redis.exists(key)
def incr(self, key: Text):
"""
使用 incr 方法,处理并发问题
当 key 不存在时,则会先初始为 0, 每次调用,则会 +1
:return:
"""
self.redis.incr(key)
def get_key(self, name: Any) -> Text:
"""
读取缓存
:param name:
:return:
"""
return self.redis.get(name)
def set_many(self, *args, **kwargs):
"""
批量设置
支持如下方式批量设置缓存
eg: set_many({'k1': 'v1', 'k2': 'v2'})
set_many(k1="v1", k2="v2")
:return:
"""
self.redis.mset(*args, **kwargs)
def get_many(self, *args):
"""获取多个值"""
results = self.redis.mget(*args)
return results
def del_all_cache(self):
"""清理所有现在的数据"""
for key in self.redis.keys():
self.del_cache(key)
def del_cache(self, name):
"""
删除缓存
:param name:
:return:
"""
self.redis.delete(name)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
日志封装,可设置不同等级的日志颜色
"""
import logging
from logging import handlers
from typing import Text
import colorlog
import time
from common.setting import ensure_path_sep
class LogHandler:
""" 日志打印封装"""
# 日志级别关系映射
level_relations = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL
}
def __init__(
self,
filename: Text,
level: Text = "info",
when: Text = "D",
fmt: Text = "%(levelname)-8s%(asctime)s%(name)s:%(filename)s:%(lineno)d %(message)s"
):
self.logger = logging.getLogger(filename)
formatter = self.log_color()
# 设置日志格式
format_str = logging.Formatter(fmt)
# 设置日志级别
self.logger.setLevel(self.level_relations.get(level))
# 往屏幕上输出
screen_output = logging.StreamHandler()
# 设置屏幕上显示的格式
screen_output.setFormatter(formatter)
# 往文件里写入#指定间隔时间自动生成文件的处理器
time_rotating = handlers.TimedRotatingFileHandler(
filename=filename,
when=when,
backupCount=3,
encoding='utf-8'
)
# 设置文件里写入的格式
time_rotating.setFormatter(format_str)
# 把对象加到logger里
self.logger.addHandler(screen_output)
self.logger.addHandler(time_rotating)
self.log_path = ensure_path_sep('\\logs\\log.log')
@classmethod
def log_color(cls):
""" 设置日志颜色 """
log_colors_config = {
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
formatter = colorlog.ColoredFormatter(
'%(log_color)s[%(asctime)s] [%(name)s] [%(levelname)s]: %(message)s',
log_colors=log_colors_config
)
return formatter
now_time_day = time.strftime("%Y-%m-%d", time.localtime())
INFO = LogHandler(ensure_path_sep(f"\\logs\\info-{now_time_day}.log"), level='info')
ERROR = LogHandler(ensure_path_sep(f"\\logs\\error-{now_time_day}.log"), level='error')
WARNING = LogHandler(ensure_path_sep(f'\\logs\\warning-{now_time_day}.log'))
if __name__ == '__main__':
ERROR.logger.error("测试")
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
日志装饰器,控制程序日志输入,默认为 True
如设置 False,则程序不会打印日志
"""
import ast
from functools import wraps
from utils.read_files_tools.regular_control import cache_regular
from utils.logging_tool.log_control import INFO, ERROR
def log_decorator(switch: bool):
"""
封装日志装饰器, 打印请求信息
:param switch: 定义日志开关
:return:
"""
def decorator(func):
@wraps(func)
def swapper(*args, **kwargs):
# 判断日志为开启状态,才打印日志
res = func(*args, **kwargs)
# 判断日志开关为开启状态
if switch:
_log_msg = f"\n======================================================\n" \
f"用例标题: {res.detail}\n" \
f"请求路径: {res.url}\n" \
f"请求方式: {res.method}\n" \
f"请求头: {res.headers}\n" \
f"请求内容: {res.request_body}\n" \
f"接口响应内容: {res.response_data}\n" \
f"接口响应时长: {res.res_time} ms\n" \
f"Http状态码: {res.status_code}\n" \
"====================================================="
_is_run = ast.literal_eval(cache_regular(str(res.is_run)))
# 判断正常打印的日志,控制台输出绿色
if _is_run in (True, None) and res.status_code == 200:
INFO.logger.info(_log_msg)
else:
# 失败的用例,控制台打印红色
ERROR.logger.error(_log_msg)
return res
return swapper
return decorator
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
统计请求运行时长装饰器,如请求响应时间超时
程序中会输入红色日志,提示时间 http 请求超时,默认时长为 3000ms
"""
from utils.logging_tool.log_control import ERROR
def execution_duration(number: int):
"""
封装统计函数执行时间装饰器
:param number: 函数预计运行时长
:return:
"""
def decorator(func):
def swapper(*args, **kwargs):
res = func(*args, **kwargs)
run_time = res.res_time
# 计算时间戳毫米级别,如果时间大于number,则打印 函数名称 和运行时间
if run_time > number:
ERROR.logger.error(
"\n==============================================\n"
"测试用例执行时间较长,请关注.\n"
"函数运行时间: %s ms\n"
"测试用例相关数据: %s\n"
"================================================="
, run_time, res)
return res
return swapper
return decorator
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
mysql 封装,支持 增、删、改、查
"""
import ast
import datetime
import decimal
from warnings import filterwarnings
import pymysql
from typing import List, Union, Text, Dict
from utils import config
from utils.logging_tool.log_control import ERROR
from utils.read_files_tools.regular_control import sql_regular
from utils.read_files_tools.regular_control import cache_regular
from utils.other_tools.exceptions import DataAcquisitionFailed, ValueTypeError
# 忽略 Mysql 告警信息
filterwarnings("ignore", category=pymysql.Warning)
class MysqlDB:
""" mysql 封装 """
if config.mysql_db.switch:
def __init__(self):
try:
# 建立数据库连接
self.conn = pymysql.connect(
host=config.mysql_db.host,
user=config.mysql_db.user,
password=config.mysql_db.password,
port=config.mysql_db.port
)
# 使用 cursor 方法获取操作游标,得到一个可以执行sql语句,并且操作结果为字典返回的游标
self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
except AttributeError as error:
ERROR.logger.error("数据库连接失败,失败原因 %s", error)
def __del__(self):
try:
# 关闭游标
self.cur.close()
# 关闭连接
self.conn.close()
except AttributeError as error:
ERROR.logger.error("数据库连接失败,失败原因 %s", error)
def query(self, sql, state="all"):
"""
查询
:param sql:
:param state: all 是默认查询全部
:return:
"""
try:
self.cur.execute(sql)
if state == "all":
# 查询全部
data = self.cur.fetchall()
else:
# 查询单条
data = self.cur.fetchone()
return data
except AttributeError as error_data:
ERROR.logger.error("数据库连接失败,失败原因 %s", error_data)
raise
def execute(self, sql: Text):
"""
更新 、 删除、 新增
:param sql:
:return:
"""
try:
# 使用 execute 操作 sql
rows = self.cur.execute(sql)
# 提交事务
self.conn.commit()
return rows
except AttributeError as error:
ERROR.logger.error("数据库连接失败,失败原因 %s", error)
# 如果事务异常,则回滚数据
self.conn.rollback()
raise
@classmethod
def sql_data_handler(cls, query_data, data):
"""
处理部分类型sql查询出来的数据格式
@param query_data: 查询出来的sql数据
@param data: 数据池
@return:
"""
# 将sql 返回的所有内容全部放入对象中
for key, value in query_data.items():
if isinstance(value, decimal.Decimal):
data[key] = float(value)
elif isinstance(value, datetime.datetime):
data[key] = str(value)
else:
data[key] = value
return data
class SetUpMySQL(MysqlDB):
""" 处理前置sql """
def setup_sql_data(self, sql: Union[List, None]) -> Dict:
"""
处理前置请求sql
:param sql:
:return:
"""
sql = ast.literal_eval(cache_regular(str(sql)))
try:
data = {}
if sql is not None:
for i in sql:
# 判断断言类型为查询类型的时候,
if i[0:6].upper() == 'SELECT':
sql_date = self.query(sql=i)[0]
for key, value in sql_date.items():
data[key] = value
else:
self.execute(sql=i)
return data
except IndexError as exc:
raise DataAcquisitionFailed("sql 数据查询失败,请检查setup_sql语句是否正确") from exc
class AssertExecution(MysqlDB):
""" 处理断言sql数据 """
def assert_execution(self, sql: list, resp) -> dict:
"""
执行 sql, 负责处理 yaml 文件中的断言需要执行多条 sql 的场景,最终会将所有数据以对象形式返回
:param resp: 接口响应数据
:param sql: sql
:return:
"""
try:
if isinstance(sql, list):
data = {}
_sql_type = ['UPDATE', 'update', 'DELETE', 'delete', 'INSERT', 'insert']
if any(i in sql for i in _sql_type) is False:
for i in sql:
# 判断sql中是否有正则,如果有则通过jsonpath提取相关的数据
sql = sql_regular(i, resp)
if sql is not None:
# for 循环逐条处理断言 sql
query_data = self.query(sql)[0]
data = self.sql_data_handler(query_data, data)
else:
raise DataAcquisitionFailed(f"该条sql未查询出任何数据, {sql}")
else:
raise DataAcquisitionFailed("断言的 sql 必须是查询的 sql")
else:
raise ValueTypeError("sql数据类型不正确,接受的是list")
return data
except Exception as error_data:
ERROR.logger.error("数据库连接失败,失败原因 %s", error_data)
raise error_data
if __name__ == '__main__':
a = MysqlDB()
b = a.query(sql="select * from `test_obp_configure`.lottery_prize where activity_id = 3")
print(b)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
钉钉通知封装
"""
import base64
import hashlib
import hmac
import time
import urllib.parse
from typing import Any, Text
from dingtalkchatbot.chatbot import DingtalkChatbot, FeedLink
from utils.other_tools.get_local_ip import get_host_ip
from utils.other_tools.allure_data.allure_report_data import AllureFileClean, TestMetrics
from utils import config
class DingTalkSendMsg:
""" 发送钉钉通知 """
def __init__(self, metrics: TestMetrics):
self.metrics = metrics
self.timeStamp = str(round(time.time() * 1000))
def xiao_ding(self):
sign = self.get_sign()
# 从yaml文件中获取钉钉配置信息
webhook = config.ding_talk.webhook + "&timestamp=" + self.timeStamp + "&sign=" + sign
return DingtalkChatbot(webhook)
def get_sign(self) -> Text:
"""
根据时间戳 + "sign" 生成密钥
:return:
"""
string_to_sign = f'{self.timeStamp}\n{config.ding_talk.secret}'.encode('utf-8')
hmac_code = hmac.new(
config.ding_talk.secret.encode('utf-8'),
string_to_sign,
digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return sign
def send_text(
self,
msg: Text,
mobiles=None
) -> None:
"""
发送文本信息
:param msg: 文本内容
:param mobiles: 艾特用户电话
:return:
"""
if not mobiles:
self.xiao_ding().send_text(msg=msg, is_at_all=True)
else:
if isinstance(mobiles, list):
self.xiao_ding().send_text(msg=msg, at_mobiles=mobiles)
else:
raise TypeError("mobiles类型错误 不是list类型.")
def send_link(
self,
title: Text,
text: Text,
message_url: Text,
pic_url: Text
) -> None:
"""
发送link通知
:return:
"""
self.xiao_ding().send_link(
title=title,
text=text,
message_url=message_url,
pic_url=pic_url
)
def send_markdown(
self,
title: Text,
msg: Text,
mobiles=None,
is_at_all=False
) -> None:
"""
:param is_at_all:
:param mobiles:
:param title:
:param msg:
markdown 格式
"""
if mobiles is None:
self.xiao_ding().send_markdown(title=title, text=msg, is_at_all=is_at_all)
else:
if isinstance(mobiles, list):
self.xiao_ding().send_markdown(title=title, text=msg, at_mobiles=mobiles)
else:
raise TypeError("mobiles类型错误 不是list类型.")
@staticmethod
def feed_link(
title: Text,
message_url: Text,
pic_url: Text
) -> Any:
""" FeedLink 二次封装 """
return FeedLink(
title=title,
message_url=message_url,
pic_url=pic_url
)
def send_feed_link(self, *arg) -> None:
"""发送 feed_lik """
self.xiao_ding().send_feed_card(list(arg))
def send_ding_notification(self):
""" 发送钉钉报告通知 """
# 判断如果有失败的用例,@所有人
is_at_all = False
if self.metrics.failed + self.metrics.broken > 0:
is_at_all = True
text = f"#### {config.project_name}自动化通知 " \
f"\n\n>Python脚本任务: {config.project_name}" \
f"\n\n>环境: TEST\n\n>" \
f"执行人: {config.tester_name}" \
f"\n\n>执行结果: {self.metrics.pass_rate}% " \
f"\n\n>总用例数: {self.metrics.total} " \
f"\n\n>成功用例数: {self.metrics.passed}" \
f" \n\n>失败用例数: {self.metrics.failed} " \
f" \n\n>异常用例数: {self.metrics.broken} " \
f"\n\n>跳过用例数: {self.metrics.skipped}" \
f" ![screenshot](" \
f"https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" \
f")\n" \
f" > ###### 测试报告 [详情](http://{get_host_ip()}:9999/index.html) \n"
DingTalkSendMsg(AllureFileClean().get_case_count()).send_markdown(
title="【接口自动化通知】",
msg=text,
is_at_all=is_at_all
)
if __name__ == '__main__':
DingTalkSendMsg(AllureFileClean().get_case_count()).send_ding_notification()
"""
发送飞书通知
"""
import json
import logging
import time
import datetime
import requests
import urllib3
from utils.other_tools.allure_data.allure_report_data import TestMetrics
from utils import config
urllib3.disable_warnings()
try:
JSONDecodeError = json.decoder.JSONDecodeError
except AttributeError:
JSONDecodeError = ValueError
def is_not_null_and_blank_str(content):
"""
非空字符串
:param content: 字符串
:return: 非空 - True,空 - False
"""
return bool(content and content.strip())
class FeiShuTalkChatBot:
"""飞书机器人通知"""
def __init__(self, metrics: TestMetrics):
self.metrics = metrics
def send_text(self, msg: str):
"""
消息类型为text类型
:param msg: 消息内容
:return: 返回消息发送结果
"""
data = {"msg_type": "text", "at": {}}
if is_not_null_and_blank_str(msg): # 传入msg非空
data["content"] = {"text": msg}
else:
logging.error("text类型,消息内容不能为空!")
raise ValueError("text类型,消息内容不能为空!")
logging.debug('text类型:%s', data)
return self.post()
def post(self):
"""
发送消息(内容UTF-8编码)
:return: 返回消息发送结果
"""
rich_text = {
"email": "1603453211@qq.com",
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": "【自动化测试通知】",
"content": [
[
{
"tag": "a",
"text": "测试报告",
"href": "https://192.168.xx.72:8080"
},
{
"tag": "at",
"user_id": "ou_18eac85d35a26f989317ad4f02e8bbbb"
# "text":"陈锐男"
}
],
[
{
"tag": "text",
"text": "测试 人员 : "
},
{
"tag": "text",
"text": f"{config.tester_name}"
}
],
[
{
"tag": "text",
"text": "运行 环境 : "
},
{
"tag": "text",
"text": f"{config.env}"
}
],
[{
"tag": "text",
"text": "成 功 率 : "
},
{
"tag": "text",
"text": f"{self.metrics.pass_rate} %"
}], # 成功率
[{
"tag": "text",
"text": "成功用例数 : "
},
{
"tag": "text",
"text": f"{self.metrics.passed}"
}], # 成功用例数
[{
"tag": "text",
"text": "失败用例数 : "
},
{
"tag": "text",
"text": f"{self.metrics.failed}"
}], # 失败用例数
[{
"tag": "text",
"text": "异常用例数 : "
},
{
"tag": "text",
"text": f"{self.metrics.failed}"
}], # 损坏用例数
[
{
"tag": "text",
"text": "时 间 : "
},
{
"tag": "text",
"text": f"{datetime.datetime.now().strftime('%Y-%m-%d')}"
}
],
[
{
"tag": "img",
"image_key": "d640eeea-4d2f-4cb3-88d8-c964fab53987",
"width": 300,
"height": 300
}
]
]
}
}
}
}
headers = {'Content-Type': 'application/json; charset=utf-8'}
post_data = json.dumps(rich_text)
response = requests.post(
config.lark.webhook,
headers=headers,
data=post_data,
verify=False
)
result = response.json()
if result.get('StatusCode') != 0:
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
result_msg = result['errmsg'] if result.get('errmsg', False) else '未知异常'
error_data = {
"msgtype": "text",
"text": {
"content": f"[注意-自动通知]飞书机器人消息发送失败,时间:{time_now},"
f"原因:{result_msg},请及时跟进,谢谢!"
},
"at": {
"isAtAll": False
}
}
logging.error("消息发送失败,自动通知:%s", error_data)
requests.post(config.lark.webhook, headers=headers, data=json.dumps(error_data))
return result
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
描述: 发送邮件
"""
import smtplib
from email.mime.text import MIMEText
from utils.other_tools.allure_data.allure_report_data import TestMetrics, AllureFileClean
from utils import config
class SendEmail:
""" 发送邮箱 """
def __init__(self, metrics: TestMetrics):
self.metrics = metrics
self.allure_data = AllureFileClean()
self.CaseDetail = self.allure_data.get_failed_cases_detail()
@classmethod
def send_mail(cls, user_list: list, sub, content: str) -> None:
"""
@param user_list: 发件人邮箱
@param sub:
@param content: 发送内容
@return:
"""
user = "余少琪" + "<" + config.email.send_user + ">"
message = MIMEText(content, _subtype='plain', _charset='utf-8')
message['Subject'] = sub
message['From'] = user
message['To'] = ";".join(user_list)
server = smtplib.SMTP()
server.connect(config.email.email_host)
server.login(config.email.send_user, config.email.stamp_key)
server.sendmail(user, user_list, message.as_string())
server.close()
def error_mail(self, error_message: str) -> None:
"""
执行异常邮件通知
@param error_message: 报错信息
@return:
"""
email = config.email.send_list
user_list = email.split(',') # 多个邮箱发送,config文件中直接添加 '806029174@qq.com'
sub = config.project_name + "接口自动化执行异常通知"
content = f"自动化测试执行完毕,程序中发现异常,请悉知。报错信息如下:\n{error_message}"
self.send_mail(user_list, sub, content)
def send_main(self) -> None:
"""
发送邮件
:return:
"""
email = config.email.send_list
user_list = email.split(',') # 多个邮箱发送,yaml文件中直接添加 '806029174@qq.com'
sub = config.project_name + "接口自动化报告"
content = f"""
各位同事, 大家好:
自动化用例执行完成,执行结果如下:
用例运行总数: {self.metrics.total} 个
通过用例个数: {self.metrics.passed} 个
失败用例个数: {self.metrics.failed} 个
异常用例个数: {self.metrics.broken} 个
跳过用例个数: {self.metrics.skipped} 个
成 功 率: {self.metrics.pass_rate} %
{self.allure_data.get_failed_cases_detail()}
**********************************
jenkins地址:https://121.xx.xx.47:8989/login
详细情况可登录jenkins平台查看,非相关负责人员可忽略此消息。谢谢。
"""
self.send_mail(user_list, sub, content)
if __name__ == '__main__':
SendEmail(AllureFileClean().get_case_count()).send_main()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
描述: 发送企业微信通知
"""
import requests
from utils.logging_tool.log_control import ERROR
from utils.other_tools.allure_data.allure_report_data import TestMetrics, AllureFileClean
from utils.times_tool.time_control import now_time
from utils.other_tools.get_local_ip import get_host_ip
from utils.other_tools.exceptions import SendMessageError, ValueTypeError
from utils import config
class WeChatSend:
"""
企业微信消息通知
"""
def __init__(self, metrics: TestMetrics):
self.metrics = metrics
self.headers = {"Content-Type": "application/json"}
def send_text(self, content, mentioned_mobile_list=None):
"""
发送文本类型通知
:param content: 文本内容,最长不超过2048个字节,必须是utf8编码
:param mentioned_mobile_list: 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
:return:
"""
_data = {"msgtype": "text", "text": {"content": content, "mentioned_list": None,
"mentioned_mobile_list": mentioned_mobile_list}}
if mentioned_mobile_list is None or isinstance(mentioned_mobile_list, list):
# 判断手机号码列表中得数据类型,如果为int类型,发送得消息会乱码
if len(mentioned_mobile_list) >= 1:
for i in mentioned_mobile_list:
if isinstance(i, str):
res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
if res.json()['errcode'] != 0:
ERROR.logger.error(res.json())
raise SendMessageError("企业微信「文本类型」消息发送失败")
else:
raise ValueTypeError("手机号码必须是字符串类型.")
else:
raise ValueTypeError("手机号码列表必须是list类型.")
def send_markdown(self, content):
"""
发送 MarkDown 类型消息
:param content: 消息内容,markdown形式
:return:
"""
_data = {"msgtype": "markdown", "markdown": {"content": content}}
res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
if res.json()['errcode'] != 0:
ERROR.logger.error(res.json())
raise SendMessageError("企业微信「MarkDown类型」消息发送失败")
def _upload_file(self, file):
"""
先将文件上传到临时媒体库
"""
key = config.wechat.webhook.split("key=")[1]
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={key}&type=file"
data = {"file": open(file, "rb")}
res = requests.post(url, files=data).json()
return res['media_id']
def send_file_msg(self, file):
"""
发送文件类型的消息
@return:
"""
_data = {"msgtype": "file", "file": {"media_id": self._upload_file(file)}}
res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
if res.json()['errcode'] != 0:
ERROR.logger.error(res.json())
raise SendMessageError("企业微信「file类型」消息发送失败")
def send_wechat_notification(self):
""" 发送企业微信通知 """
text = f"""【{config.project_name}自动化通知】
>测试环境:<font color=\"info\">TEST</font>
>测试负责人:@{config.tester_name}
>
> **执行结果**
><font color=\"info\">成 功 率 : {self.metrics.pass_rate}%</font>
>用例 总数:<font color=\"info\">{self.metrics.total}</font>
>成功用例数:<font color=\"info\">{self.metrics.passed}</font>
>失败用例数:`{self.metrics.failed}个`
>异常用例数:`{self.metrics.broken}个`
>跳过用例数:<font color=\"warning\">{self.metrics.skipped}个</font>
>用例执行时长:<font color=\"warning\">{self.metrics.time} s</font>
>时间:<font color=\"comment\">{now_time()}</font>
>
>非相关负责人员可忽略此消息。
>测试报告,点击查看>>[测试报告入口](http://{get_host_ip()}:9999/index.html)"""
WeChatSend(AllureFileClean().get_case_count()).send_markdown(text)
if __name__ == '__main__':
WeChatSend(AllureFileClean().get_case_count()).send_wechat_notification()
# coding=utf-8
"""
"""
from utils.mysql_tool.mysql_control import MysqlDB
import copy
class AddressDetection(MysqlDB):
def get_shop_address_entity_str(self):
"""
获取所有已经上线并且未删除的店铺地址(去除自定店铺,自营店铺没有地址)
:return:
"""
shop_info = self.query("SELECT id, name, attribute, shop_type, sub_shop_type "
"FROM `test_obp_supplier`.`supplier_shop` "
"where status = 2 and delete_flag = 0 and sub_shop_type > 300 "
"and sub_shop_type = 300")
return shop_info
def get_logistics_address_library(self):
"""
获取平台地址库中的省份code
:return:
"""
code = self.query("select name, code from `test_obp_order`.`logistics_address_library` "
"where parent_code > 0")
area_code = {}
for i in code:
area_code[i['code']] = i['name']
return area_code
def get_error_shop(self):
"""
获取错误的店铺数据
:return:
"""
# 获取区域code
get_logistics_address_library = self.get_logistics_address_library()
num = 0
for i in self.get_shop_address_entity_str():
# 获取店铺地址
shop_address_entity_str = eval(i['attribute'])['shopAddressEntityStr']
if shop_address_entity_str['countiesName'] == get_logistics_address_library[str(shop_address_entity_str['countiesCode'])]:
pass
else:
area_name = self.query(f"SELECT name, code FROM `test_obp_order`.`logistics_address_library`"
f" where parent_code = {shop_address_entity_str['cityCode']} and name = '{shop_address_entity_str['countiesName']}'")
num += 1
new_shop_address_entity_str = copy.deepcopy(shop_address_entity_str)
new_shop_address_entity_str['countiesCode'] = area_name[0]['code']
# print(str(f'update obp_supplier.supplier_shop set attribute = json_set(attribute,"$.shopAddressEntityStr.countiesCode",{area_name[0]["code"]}) where id = {i["id"]};'))
print(f"店铺名称: {i['name']}, 店铺id: {i['id']}, "
f"店铺地址:{shop_address_entity_str['cityName']}{shop_address_entity_str['provinceName']}{shop_address_entity_str['countiesName']}"
f"\n当前实际数据:{shop_address_entity_str}"
f"\n{shop_address_entity_str['countiesName']}的实际code码为 {area_name}"
f"\n更改后的数据: {new_shop_address_entity_str}")
print("*" * 100)
print(num)
AddressDetection().get_error_shop()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
描述: 收集 allure 报告
"""
import json
from typing import List, Text
from common.setting import ensure_path_sep
from utils.read_files_tools.get_all_files_path import get_all_files
from utils.other_tools.models import TestMetrics
class AllureFileClean:
"""allure 报告数据清洗,提取业务需要得数据"""
@classmethod
def get_testcases(cls) -> List:
""" 获取所有 allure 报告中执行用例的情况"""
# 将所有数据都收集到files中
files = []
for i in get_all_files(ensure_path_sep("\\report\\html\\data\\test-cases")):
with open(i, 'r', encoding='utf-8') as file:
date = json.load(file)
files.append(date)
return files
def get_failed_case(self) -> List:
""" 获取到所有失败的用例标题和用例代码路径"""
error_case = []
for i in self.get_testcases():
if i['status'] == 'failed' or i['status'] == 'broken':
error_case.append((i['name'], i['fullName']))
return error_case
def get_failed_cases_detail(self) -> Text:
""" 返回所有失败的测试用例相关内容 """
date = self.get_failed_case()
values = ""
# 判断有失败用例,则返回内容
if len(date) >= 1:
values = "失败用例:\n"
values += " **********************************\n"
for i in date:
values += " " + i[0] + ":" + i[1] + "\n"
return values
@classmethod
def get_case_count(cls) -> "TestMetrics":
""" 统计用例数量 """
try:
file_name = ensure_path_sep("\\report\\html\\widgets\\summary.json")
with open(file_name, 'r', encoding='utf-8') as file:
data = json.load(file)
_case_count = data['statistic']
_time = data['time']
keep_keys = {"passed", "failed", "broken", "skipped", "total"}
run_case_data = {k: v for k, v in data['statistic'].items() if k in keep_keys}
# 判断运行用例总数大于0
if _case_count["total"] > 0:
# 计算用例成功率
run_case_data["pass_rate"] = round(
(_case_count["passed"] + _case_count["skipped"]) / _case_count["total"] * 100, 2
)
else:
# 如果未运行用例,则成功率为 0.0
run_case_data["pass_rate"] = 0.0
# 收集用例运行时长
run_case_data['time'] = _time if run_case_data['total'] == 0 else round(_time['duration'] / 1000, 2)
return TestMetrics(**run_case_data)
except FileNotFoundError as exc:
raise FileNotFoundError(
"程序中检查到您未生成allure报告,"
"通常可能导致的原因是allure环境未配置正确,"
"详情可查看如下博客内容:"
"https://blog.csdn.net/weixin_43865008/article/details/124332793"
) from exc
if __name__ == '__main__':
AllureFileClean().get_case_count()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import json
import allure
from utils.other_tools.models import AllureAttachmentType
def allure_step(step: str, var: str) -> None:
"""
:param step: 步骤及附件名称
:param var: 附件内容
"""
with allure.step(step):
allure.attach(
json.dumps(
str(var),
ensure_ascii=False,
indent=4),
step,
allure.attachment_type.JSON)
def allure_attach(source: str, name: str, extension: str):
"""
allure报告上传附件、图片、excel等
:param source: 文件路径,相当于传一个文件
:param name: 附件名称
:param extension: 附件的拓展名称
:return:
"""
# 获取上传附件的尾缀,判断对应的 attachment_type 枚举值
_name = name.split('.')[-1].upper()
_attachment_type = getattr(AllureAttachmentType, _name, None)
allure.attach.file(
source=source,
name=name,
attachment_type=_attachment_type if _attachment_type is None else _attachment_type.value,
extension=extension
)
def allure_step_no(step: str):
"""
无附件的操作步骤
:param step: 步骤名称
:return:
"""
with allure.step(step):
pass
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
"""
import json
import shutil
import ast
import xlwings
from common.setting import ensure_path_sep
from utils.read_files_tools.get_all_files_path import get_all_files
from utils.notify.wechat_send import WeChatSend
from utils.other_tools.allure_data.allure_report_data import AllureFileClean
# TODO 还需要处理动态值
class ErrorTestCase:
""" 收集错误的excel """
def __init__(self):
self.test_case_path = ensure_path_sep("\\report\\html\\data\\test-cases\\")
def get_error_case_data(self):
"""
收集所有失败用例的数据
@return:
"""
path = get_all_files(self.test_case_path)
files = []
for i in path:
with open(i, 'r', encoding='utf-8') as file:
date = json.load(file)
# 收集执行失败的用例数据
if date['status'] == 'failed' or date['status'] == 'broken':
files.append(date)
print(files)
return files
@classmethod
def get_case_name(cls, test_case):
"""
收集测试用例名称
@return:
"""
name = test_case['name'].split('[')
case_name = name[1][:-1]
return case_name
@classmethod
def get_parameters(cls, test_case):
"""
获取allure报告中的 parameters 参数内容, 请求前的数据
用于兼容用例执行异常,未发送请求导致的情况
@return:
"""
parameters = test_case['parameters'][0]['value']
return ast.literal_eval(parameters)
@classmethod
def get_test_stage(cls, test_case):
"""
获取allure报告中请求后的数据
@return:
"""
test_stage = test_case['testStage']['steps']
return test_stage
def get_case_url(self, test_case):
"""
获取测试用例的 url
@param test_case:
@return:
"""
# 判断用例步骤中的数据是否异常
if test_case['testStage']['status'] == 'broken':
# 如果异常状态下,则获取请求前的数据
_url = self.get_parameters(test_case)['url']
else:
# 否则拿请求步骤的数据,因为如果设计到依赖,会获取多组,因此我们只取最后一组数据内容
_url = self.get_test_stage(test_case)[-7]['name'][7:]
return _url
def get_method(self, test_case):
"""
获取用例中的请求方式
@param test_case:
@return:
"""
if test_case['testStage']['status'] == 'broken':
_method = self.get_parameters(test_case)['method']
else:
_method = self.get_test_stage(test_case)[-6]['name'][6:]
return _method
def get_headers(self, test_case):
"""
获取用例中的请求头
@return:
"""
if test_case['testStage']['status'] == 'broken':
_headers = self.get_parameters(test_case)['headers']
else:
# 如果用例请求成功,则从allure附件中获取请求头部信息
_headers_attachment = self.get_test_stage(test_case)[-5]['attachments'][0]['source']
path = ensure_path_sep("\\report\\html\\data\\attachments\\" + _headers_attachment)
with open(path, 'r', encoding='utf-8') as file:
_headers = json.load(file)
return _headers
def get_request_type(self, test_case):
"""
获取用例的请求类型
@param test_case:
@return:
"""
request_type = self.get_parameters(test_case)['requestType']
return request_type
def get_case_data(self, test_case):
"""
获取用例内容
@return:
"""
if test_case['testStage']['status'] == 'broken':
_case_data = self.get_parameters(test_case)['data']
else:
_case_data_attachments = self.get_test_stage(test_case)[-4]['attachments'][0]['source']
path = ensure_path_sep("\\report\\html\\data\\attachments\\" + _case_data_attachments)
with open(path, 'r', encoding='utf-8') as file:
_case_data = json.load(file)
return _case_data
def get_dependence_case(self, test_case):
"""
获取依赖用例
@param test_case:
@return:
"""
_dependence_case_data = self.get_parameters(test_case)['dependence_case_data']
return _dependence_case_data
def get_sql(self, test_case):
"""
获取 sql 数据
@param test_case:
@return:
"""
sql = self.get_parameters(test_case)['sql']
return sql
def get_assert(self, test_case):
"""
获取断言数据
@param test_case:
@return:
"""
assert_data = self.get_parameters(test_case)['assert_data']
return assert_data
@classmethod
def get_response(cls, test_case):
"""
获取响应内容的数据
@param test_case:
@return:
"""
if test_case['testStage']['status'] == 'broken':
_res_date = test_case['testStage']['statusMessage']
else:
try:
res_data_attachments = \
test_case['testStage']['steps'][-1]['attachments'][0]['source']
path = ensure_path_sep("\\report\\html\\data\\attachments\\" + res_data_attachments)
with open(path, 'r', encoding='utf-8') as file:
_res_date = json.load(file)
except FileNotFoundError:
# 程序中没有提取到响应数据,返回None
_res_date = None
return _res_date
@classmethod
def get_case_time(cls, test_case):
"""
获取用例运行时长
@param test_case:
@return:
"""
case_time = str(test_case['time']['duration']) + "ms"
return case_time
@classmethod
def get_uid(cls, test_case):
"""
获取 allure 报告中的 uid
@param test_case:
@return:
"""
uid = test_case['uid']
return uid
class ErrorCaseExcel:
""" 收集运行失败的用例,整理成excel报告 """
def __init__(self):
_excel_template = ensure_path_sep("\\utils\\other_tools\\allure_data\\自动化异常测试用例.xlsx")
self._file_path = ensure_path_sep("\\Files\\" + "自动化异常测试用例.xlsx")
# if os.path.exists(self._file_path):
# os.remove(self._file_path)
shutil.copyfile(src=_excel_template, dst=self._file_path)
# 打开程序(只打开不新建)
self.app = xlwings.App(visible=False, add_book=False)
self.w_book = self.app.books.open(self._file_path, read_only=False)
# 选取工作表:
self.sheet = self.w_book.sheets['异常用例'] # 或通过索引选取
self.case_data = ErrorTestCase()
def background_color(self, position: str, rgb: tuple):
"""
excel 单元格设置背景色
@param rgb: rgb 颜色 rgb=(0,255,0)
@param position: 位置,如 A1, B1...
@return:
"""
# 定位到单元格位置
rng = self.sheet.range(position)
excel_rgb = rng.color = rgb
return excel_rgb
def column_width(self, position: str, width: int):
"""
设置列宽
@return:
"""
rng = self.sheet.range(position)
# 列宽
excel_column_width = rng.column_width = width
return excel_column_width
def row_height(self, position, height):
"""
设置行高
@param position:
@param height:
@return:
"""
rng = self.sheet.range(position)
excel_row_height = rng.row_height = height
return excel_row_height
def column_width_adaptation(self, position):
"""
excel 所有列宽度自适应
@return:
"""
rng = self.sheet.range(position)
auto_fit = rng.columns.autofit()
return auto_fit
def row_width_adaptation(self, position):
"""
excel 设置所有行宽自适应
@return:
"""
rng = self.sheet.range(position)
row_adaptation = rng.rows.autofit()
return row_adaptation
def write_excel_content(self, position: str, value: str):
"""
excel 中写入内容
@param value:
@param position:
@return:
"""
self.sheet.range(position).value = value
def write_case(self):
"""
用例中写入失败用例数据
@return:
"""
_data = self.case_data.get_error_case_data()
# 判断有数据才进行写入
if len(_data) > 0:
num = 2
for data in _data:
self.write_excel_content(position="A" + str(num), value=str(self.case_data.get_uid(data)))
self.write_excel_content(position='B' + str(num), value=str(self.case_data.get_case_name(data)))
self.write_excel_content(position="C" + str(num), value=str(self.case_data.get_case_url(data)))
self.write_excel_content(position="D" + str(num), value=str(self.case_data.get_method(data)))
self.write_excel_content(position="E" + str(num), value=str(self.case_data.get_request_type(data)))
self.write_excel_content(position="F" + str(num), value=str(self.case_data.get_headers(data)))
self.write_excel_content(position="G" + str(num), value=str(self.case_data.get_case_data(data)))
self.write_excel_content(position="H" + str(num), value=str(self.case_data.get_dependence_case(data)))
self.write_excel_content(position="I" + str(num), value=str(self.case_data.get_assert(data)))
self.write_excel_content(position="J" + str(num), value=str(self.case_data.get_sql(data)))
self.write_excel_content(position="K" + str(num), value=str(self.case_data.get_case_time(data)))
self.write_excel_content(position="L" + str(num), value=str(self.case_data.get_response(data)))
num += 1
self.w_book.save()
self.w_book.close()
self.app.quit()
# 有数据才发送企业微信
WeChatSend(AllureFileClean().get_case_count()).send_file_msg(self._file_path)
if __name__ == '__main__':
ErrorCaseExcel().write_case()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
描述:
"""
class MyBaseFailure(Exception):
pass
class JsonpathExtractionFailed(MyBaseFailure):
pass
class NotFoundError(MyBaseFailure):
pass
class FileNotFound(FileNotFoundError, NotFoundError):
pass
class SqlNotFound(NotFoundError):
pass
class AssertTypeError(MyBaseFailure):
pass
class DataAcquisitionFailed(MyBaseFailure):
pass
class ValueTypeError(MyBaseFailure):
pass
class SendMessageError(MyBaseFailure):
pass
class ValueNotFoundError(MyBaseFailure):
pass
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import socket
def get_host_ip():
"""
查询本机ip地址
:return:
"""
_s = None
try:
_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
_s.connect(('8.8.8.8', 80))
l_host = _s.getsockname()[0]
finally:
_s.close()
return l_host
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
# @Time : 2022/5/10 14:02
# @Author : 余少琪
# @Email : 1603453211@qq.com
# @File : install_requirements
# @describe: 判断程序是否每次会更新依赖库,如有更新,则自动安装
"""
import os
import chardet
from common.setting import ensure_path_sep
from utils.logging_tool.log_control import INFO
from utils import config
os.system("pip3 install chardet")
class InstallRequirements:
""" 自动识别安装最新的依赖库 """
def __init__(self):
self.version_library_comparisons_path = ensure_path_sep("\\utils\\other_tools\\install_tool\\") \
+ "version_library_comparisons.txt"
self.requirements_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) \
+ os.sep + "requirements.txt"
self.mirror_url = config.mirror_source
# 初始化时,获取最新的版本库
# os.system("pip freeze > {0}".format(self.requirements_path))
def read_version_library_comparisons_txt(self):
"""
获取版本比对默认的文件
@return:
"""
with open(self.version_library_comparisons_path, 'r', encoding="utf-8") as file:
return file.read().strip(' ')
@classmethod
def check_charset(cls, file_path):
"""获取文件的字符集"""
with open(file_path, "rb") as file:
data = file.read(4)
charset = chardet.detect(data)['encoding']
return charset
def read_requirements(self):
"""获取安装文件"""
file_data = ""
with open(
self.requirements_path,
'r',
encoding=self.check_charset(self.requirements_path)
) as file:
for line in file:
if "" in line:
line = line.replace("", "")
file_data += line
with open(
self.requirements_path,
"w",
encoding=self.check_charset(self.requirements_path)
) as file:
file.write(file_data)
return file_data
def text_comparison(self):
"""
版本库比对
@return:
"""
read_version_library_comparisons_txt = self.read_version_library_comparisons_txt()
read_requirements = self.read_requirements()
if read_version_library_comparisons_txt == read_requirements:
INFO.logger.info("程序中未检查到更新版本库,已为您跳过自动安装库")
# 程序中如出现不同的文件,则安装
else:
INFO.logger.info("程序中检测到您更新了依赖库,已为您自动安装")
os.system(f"pip3 install -r {self.requirements_path}")
with open(self.version_library_comparisons_path, "w",
encoding=self.check_charset(self.requirements_path)) as file:
file.write(read_requirements)
if __name__ == '__main__':
InstallRequirements().text_comparison()
aiofiles==0.8.0
allure-pytest==2.9.45
allure-python-commons==2.9.45
asgiref==3.5.1
atomicwrites==1.4.0
attrs==21.2.0
blinker==1.4
Brotli==1.0.9
certifi==2021.10.8
cffi==1.15.0
chardet==4.0.0
charset-normalizer==2.0.7
click==8.1.3
colorama==0.4.4
colorlog==6.6.0
cryptography==36.0.0
DingtalkChatbot==1.5.3
et-xmlfile==1.1.0
execnet==1.9.0
Faker==9.8.3
Flask==2.0.3
h11==0.13.0
h2==4.1.0
hpack==4.0.0
httptools==0.4.0
hyperframe==6.0.1
idna==3.3
iniconfig==1.1.0
itchat==1.3.10
itsdangerous==2.1.2
Jinja2==3.1.2
jsonpath==0.82
kaitaistruct==0.9
ldap3==2.9.1
MarkupSafe==2.1.1
mitmproxy==8.0.0
msgpack==1.0.3
multidict==6.0.2
openpyxl==3.0.9
packaging==21.3
passlib==1.7.4
pluggy==1.0.0
protobuf==3.19.4
publicsuffix2==2.20191221
py==1.11.0
pyasn1==0.4.8
pycparser==2.21
pydivert==2.1.0
PyMySQL==1.0.2
pyOpenSSL==21.0.0
pyparsing==3.0.6
pyperclip==1.8.2
pypng==0.0.21
PyQRCode==1.2.1
pytest==6.2.5
pytest-forked==1.3.0
pytest-xdist==2.4.0
python-dateutil==2.8.2
pywin32==304
PyYAML==6.0
requests==2.26.0
requests-toolbelt==0.9.1
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.6
sanic==22.3.1
sanic-routing==22.3.0
six==1.16.0
sortedcontainers==2.4.0
text-unidecode==1.3
toml==0.10.2
tornado==6.1
urllib3==1.26.7
urwid==2.1.2
websockets==10.3
Werkzeug==2.1.2
wsproto==1.1.0
xlrd==2.0.1
xlutils==2.0.0
xlwings==0.27.7
xlwt==1.3.0
zstandard==0.17.0
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
"""
def jsonpath_replace(change_data, key_name, data_switch=None):
"""处理jsonpath数据"""
_new_data = key_name + ''
for i in change_data:
if i == '$':
pass
elif data_switch is None and i == "data":
_new_data += '.data'
elif i[0] == '[' and i[-1] == ']':
_new_data += "[" + i[1:-1] + "]"
else:
_new_data += '[' + '"' + i + '"' + "]"
return _new_data
if __name__ == '__main__':
jsonpath_replace(change_data=['$', 'data', 'id'], key_name='self.__yaml_case')
import types
from enum import Enum, unique
from typing import Text, Dict, Callable, Union, Optional, List, Any
from dataclasses import dataclass
from pydantic import BaseModel, Field
class NotificationType(Enum):
""" 自动化通知方式 """
DEFAULT = '0'
DING_TALK = '1'
WECHAT = '2'
EMAIL = '3'
FEI_SHU = '4'
@dataclass
class TestMetrics:
""" 用例执行数据 """
passed: int
failed: int
broken: int
skipped: int
total: int
pass_rate: float
time: Text
class RequestType(Enum):
"""
request请求发送,请求参数的数据类型
"""
JSON = "JSON"
PARAMS = "PARAMS"
DATA = "DATA"
FILE = 'FILE'
EXPORT = "EXPORT"
NONE = "NONE"
class TestCaseEnum(Enum):
URL = ("url", True)
HOST = ("host", True)
METHOD = ("method", True)
DETAIL = ("detail", True)
IS_RUN = ("is_run", True)
HEADERS = ("headers", True)
REQUEST_TYPE = ("requestType", True)
DATA = ("data", True)
DE_CASE = ("dependence_case", True)
DE_CASE_DATA = ("dependence_case_data", False)
CURRENT_RE_SET_CACHE = ("current_request_set_cache", False)
SQL = ("sql", False)
ASSERT_DATA = ("assert", True)
SETUP_SQL = ("setup_sql", False)
TEARDOWN = ("teardown", False)
TEARDOWN_SQL = ("teardown_sql", False)
SLEEP = ("sleep", False)
class Method(Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
PATCH = "PATCH"
DELETE = "DELETE"
HEAD = "HEAD"
OPTION = "OPTION"
def load_module_functions(module) -> Dict[Text, Callable]:
""" 获取 module中方法的名称和所在的内存地址 """
module_functions = {}
for name, item in vars(module).items():
if isinstance(item, types.FunctionType):
module_functions[name] = item
return module_functions
@unique
class DependentType(Enum):
"""
数据依赖相关枚举
"""
RESPONSE = 'response'
REQUEST = 'request'
SQL_DATA = 'sqlData'
CACHE = "cache"
class Assert(BaseModel):
jsonpath: Text
type: Text
value: Any
AssertType: Union[None, Text] = None
class DependentData(BaseModel):
dependent_type: Text
jsonpath: Text
set_cache: Optional[Text]
replace_key: Optional[Text]
class DependentCaseData(BaseModel):
case_id: Text
# dependent_data: List[DependentData]
dependent_data: Union[None, List[DependentData]] = None
class ParamPrepare(BaseModel):
dependent_type: Text
jsonpath: Text
set_cache: Text
class SendRequest(BaseModel):
dependent_type: Text
jsonpath: Optional[Text]
cache_data: Optional[Text]
set_cache: Optional[Text]
replace_key: Optional[Text]
class TearDown(BaseModel):
case_id: Text
param_prepare: Optional[List["ParamPrepare"]]
send_request: Optional[List["SendRequest"]]
class CurrentRequestSetCache(BaseModel):
type: Text
jsonpath: Text
name: Text
class TestCase(BaseModel):
url: Text
method: Text
detail: Text
# assert_data: Union[Dict, Text] = Field(..., alias="assert")
assert_data: Union[Dict, Text]
headers: Union[None, Dict, Text] = {}
requestType: Text
is_run: Union[None, bool, Text] = None
data: Any = None
dependence_case: Union[None, bool] = False
dependence_case_data: Optional[Union[None, List["DependentCaseData"], Text]] = None
sql: List = None
setup_sql: List = None
status_code: Optional[int] = None
teardown_sql: Optional[List] = None
teardown: Union[List["TearDown"], None] = None
current_request_set_cache: Optional[List["CurrentRequestSetCache"]]
sleep: Optional[Union[int, float]]
class ResponseData(BaseModel):
url: Text
is_run: Union[None, bool, Text]
detail: Text
response_data: Text
request_body: Any
method: Text
sql_data: Dict
yaml_data: "TestCase"
headers: Dict
cookie: Dict
assert_data: Dict
res_time: Union[int, float]
status_code: int
teardown: List["TearDown"] = None
teardown_sql: Union[None, List]
body: Any
class DingTalk(BaseModel):
webhook: Union[Text, None]
secret: Union[Text, None]
class MySqlDB(BaseModel):
switch: bool = False
host: Union[Text, None] = None
user: Union[Text, None] = None
password: Union[Text, None] = None
port: Union[int, None] = 3306
class Webhook(BaseModel):
webhook: Union[Text, None]
class Email(BaseModel):
send_user: Union[Text, None]
email_host: Union[Text, None]
stamp_key: Union[Text, None]
# 收件人
send_list: Union[Text, None]
class Config(BaseModel):
project_name: Text
env: Text
tester_name: Text
notification_type: Text = '0'
excel_report: bool
ding_talk: "DingTalk"
mysql_db: "MySqlDB"
mirror_source: Text
wechat: "Webhook"
email: "Email"
lark: "Webhook"
real_time_update_test_cases: bool = False
host: Text
app_host: Union[Text, None]
@unique
class AllureAttachmentType(Enum):
"""
allure 报告的文件类型枚举
"""
TEXT = "txt"
CSV = "csv"
TSV = "tsv"
URI_LIST = "uri"
HTML = "html"
XML = "xml"
JSON = "json"
YAML = "yaml"
PCAP = "pcap"
PNG = "png"
JPG = "jpg"
SVG = "svg"
GIF = "gif"
BMP = "bmp"
TIFF = "tiff"
MP4 = "mp4"
OGG = "ogg"
WEBM = "webm"
PDF = "pdf"
@unique
class AssertMethod(Enum):
"""断言类型"""
equals = "=="
less_than = "lt"
less_than_or_equals = "le"
greater_than = "gt"
greater_than_or_equals = "ge"
not_equals = "not_eq"
string_equals = "str_eq"
length_equals = "len_eq"
length_greater_than = "len_gt"
length_greater_than_or_equals = 'len_ge'
length_less_than = "len_lt"
length_less_than_or_equals = 'len_le'
contains = "contains"
contained_by = 'contained_by'
startswith = 'startswith'
endswith = 'endswith'
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import time
import threading
class PyTimer:
"""定时器类"""
def __init__(self, func, *args, **kwargs):
"""构造函数"""
self.func = func
self.args = args
self.kwargs = kwargs
self.running = False
def _run_func(self):
"""运行定时事件函数"""
_thread = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs)
_thread.setDaemon(True)
_thread.start()
def _start(self, interval, once):
"""启动定时器的线程函数"""
interval = max(interval, 0.01)
if interval < 0.050:
_dt = interval / 10
else:
_dt = 0.005
if once:
deadline = time.time() + interval
while time.time() < deadline:
time.sleep(_dt)
# 定时时间到,调用定时事件函数
self._run_func()
else:
self.running = True
deadline = time.time() + interval
while self.running:
while time.time() < deadline:
time.sleep(_dt)
# 更新下一次定时时间
deadline += interval
# 定时时间到,调用定时事件函数
if self.running:
self._run_func()
def start(self, interval, once=False):
"""启动定时器
interval - 定时间隔,浮点型,以秒为单位,最高精度10毫秒
once - 是否仅启动一次,默认是连续的
"""
thread_ = threading.Thread(target=self._start, args=(interval, once))
thread_.setDaemon(True)
thread_.start()
def stop(self):
"""停止定时器"""
self.running = False
def do_something(name, gender='male'):
"""执行"""
print(time.time(), '定时时间到,执行特定任务')
print('name:%s, gender:%s', name, gender)
time.sleep(5)
print(time.time(), '完成特定任务')
timer = PyTimer(do_something, 'Alice', gender='female')
timer.start(0.5, once=False)
input('按回车键结束\n') # 此处阻塞住进程
timer.stop()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import os
from typing import Text, Dict
from common.setting import ensure_path_sep
from utils.read_files_tools.testcase_template import write_testcase_file
from utils.read_files_tools.yaml_control import GetYamlData
from utils.read_files_tools.get_all_files_path import get_all_files
from utils.other_tools.exceptions import ValueNotFoundError
class TestCaseAutomaticGeneration:
def __init__(self):
self.yaml_case_data = None
self.file_path = None
@property
def case_date_path(self) -> Text:
"""返回 yaml 用例文件路径"""
return ensure_path_sep("\\data")
@property
def case_path(self) -> Text:
""" 存放用例代码路径"""
return ensure_path_sep("\\test_case")
@property
def allure_epic(self):
_allure_epic = self.yaml_case_data.get("case_common").get("allureEpic")
assert _allure_epic is not None, (
"用例中 allureEpic 为必填项,请检查用例内容, 用例路径:'%s'" % self.file_path
)
return _allure_epic
@property
def allure_feature(self):
_allure_feature = self.yaml_case_data.get("case_common").get("allureFeature")
assert _allure_feature is not None, (
"用例中 allureFeature 为必填项,请检查用例内容, 用例路径:'%s'" % self.file_path
)
return _allure_feature
@property
def allure_story(self):
_allure_story = self.yaml_case_data.get("case_common").get("allureStory")
assert _allure_story is not None, (
"用例中 allureStory 为必填项,请检查用例内容, 用例路径:'%s'" % self.file_path
)
return _allure_story
@property
def file_name(self) -> Text:
"""
通过 yaml文件的命名,将名称转换成 py文件的名称
:return: 示例: DateDemo.py
"""
i = len(self.case_date_path)
yaml_path = self.file_path[i:]
file_name = None
# 路径转换
if '.yaml' in yaml_path:
file_name = yaml_path.replace('.yaml', '.py')
elif '.yml' in yaml_path:
file_name = yaml_path.replace('.yml', '.py')
return file_name
@property
def get_test_class_title(self):
"""
自动生成类名称
:return: sup_apply_list --> SupApplyList
"""
# 提取文件名称
_file_name = os.path.split(self.file_name)[1][:-3]
_name = _file_name.split("_")
_name_len = len(_name)
# 将文件名称格式,转换成类名称: sup_apply_list --> SupApplyList
for i in range(_name_len):
_name[i] = _name[i].capitalize()
_class_name = "".join(_name)
return _class_name
@property
def func_title(self) -> Text:
"""
函数名称
:return:
"""
return os.path.split(self.file_name)[1][:-3]
@property
def spilt_path(self):
path = self.file_name.split(os.sep)
path[-1] = path[-1].replace(path[-1], "test_" + path[-1])
return path
@property
def get_case_path(self):
"""
根据 yaml 中的用例,生成对应 testCase 层代码的路径
:return: D:\\Project\\test_case\\test_case_demo.py
"""
new_name = os.sep.join(self.spilt_path)
return ensure_path_sep("\\test_case" + new_name)
@property
def case_ids(self):
return [k for k in self.yaml_case_data.keys() if k != "case_common"]
@property
def get_file_name(self):
# 这里通过“\\” 符号进行分割,提取出来文件名称
# 判断生成的 testcase 文件名称,需要以test_ 开头
case_name = self.spilt_path[-1].replace(
self.spilt_path[-1], "test_" + self.spilt_path[-1]
)
return case_name
def mk_dir(self) -> None:
""" 判断生成自动化代码的文件夹路径是否存在,如果不存在,则自动创建 """
# _LibDirPath = os.path.split(self.libPagePath(filePath))[0]
_case_dir_path = os.path.split(self.get_case_path)[0]
if not os.path.exists(_case_dir_path):
os.makedirs(_case_dir_path)
def get_case_automatic(self) -> None:
""" 自动生成 测试代码"""
file_path = get_all_files(file_path=ensure_path_sep("\\data"), yaml_data_switch=True)
for file in file_path:
# 判断代理拦截的yaml文件,不生成test_case代码
if 'proxy_data.yaml' not in file:
# 判断用例需要用的文件夹路径是否存在,不存在则创建
self.yaml_case_data = GetYamlData(file).get_yaml_data()
self.file_path = file
self.mk_dir()
write_testcase_file(
allure_epic=self.allure_epic,
allure_feature=self.allure_feature,
class_title=self.get_test_class_title,
func_title=self.func_title,
case_path=self.get_case_path,
case_ids=self.case_ids,
file_name=self.get_file_name,
allure_story=self.allure_story
)
if __name__ == '__main__':
TestCaseAutomaticGeneration().get_case_automatic()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import os
def del_file(path):
"""删除目录下的文件"""
list_path = os.listdir(path)
for i in list_path:
c_path = os.path.join(path, i)
if os.path.isdir(c_path):
del_file(c_path)
else:
os.remove(c_path)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import json
import xlrd
from xlutils.copy import copy
from common.setting import ensure_path_sep
def get_excel_data(sheet_name: str, case_name: any) -> list:
"""
读取 Excel 中的数据
:param sheet_name: excel 中的 sheet 页的名称
:param case_name: 测试用例名称
:return:
"""
res_list = []
excel_dire = ensure_path_sep("\\data\\TestLogin.xls")
work_book = xlrd.open_workbook(excel_dire, formatting_info=True)
# 打开对应的子表
work_sheet = work_book.sheet_by_name(sheet_name)
# 读取一行
idx = 0
for one in work_sheet.col_values(0):
# 运行需要运行的测试用例
if case_name in one:
req_body_data = work_sheet.cell(idx, 9).value
resp_data = work_sheet.cell(idx, 11).value
res_list.append((req_body_data, json.loads(resp_data)))
idx += 1
return res_list
def set_excel_data(sheet_index: int) -> tuple:
"""
excel 写入
:return:
"""
excel_dire = '../data/TestLogin.xls'
work_book = xlrd.open_workbook(excel_dire, formatting_info=True)
work_book_new = copy(work_book)
work_sheet_new = work_book_new.get_sheet(sheet_index)
return work_book_new, work_sheet_new
if __name__ == '__main__':
get_excel_data("异常用例", '111')
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import os
def get_all_files(file_path, yaml_data_switch=False) -> list:
"""
获取文件路径
:param file_path: 目录路径
:param yaml_data_switch: 是否过滤文件为 yaml格式, True则过滤
:return:
"""
filename = []
# 获取所有文件下的子文件名称
for root, dirs, files in os.walk(file_path):
for _file_path in files:
path = os.path.join(root, _file_path)
if yaml_data_switch:
if 'yaml' in path or '.yml' in path:
filename.append(path)
else:
filename.append(path)
return filename
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
from enum import Enum
from typing import Union, Text, Dict, List
from utils.read_files_tools.yaml_control import GetYamlData
from utils.other_tools.models import TestCase
from utils.other_tools.exceptions import ValueNotFoundError
from utils.cache_process.cache_control import CacheHandler
from utils import config
from utils.other_tools.models import RequestType, Method, TestCaseEnum
import os
class CaseDataCheck:
"""
yaml 数据解析, 判断数据填写是否符合规范
"""
def __init__(self, file_path):
self.file_path = file_path
if os.path.exists(self.file_path) is False:
raise FileNotFoundError("用例地址未找到")
self.case_data = None
self.case_id = None
def _assert(self, attr: Text):
assert attr in self.case_data.keys(), (
f"用例ID为 {self.case_id} 的用例中缺少 {attr} 参数,请确认用例内容是否编写规范."
f"当前用例文件路径:{self.file_path}"
)
def check_params_exit(self):
for enum in list(TestCaseEnum._value2member_map_.keys()):
if enum[1]:
self._assert(enum[0])
def check_params_right(self, enum_name, attr):
_member_names_ = enum_name._member_names_
assert attr.upper() in _member_names_, (
f"用例ID为 {self.case_id} 的用例中 {attr} 填写不正确,"
f"当前框架中只支持 {_member_names_} 类型."
f"如需新增 method 类型,请联系管理员."
f"当前用例文件路径:{self.file_path}"
)
return attr.upper()
@property
def get_method(self) -> Text:
return self.check_params_right(
Method,
self.case_data.get(TestCaseEnum.METHOD.value[0])
)
@property
def get_host(self) -> Text:
host = (
self.case_data.get(TestCaseEnum.HOST.value[0]) +
self.case_data.get(TestCaseEnum.URL.value[0])
)
return host
@property
def get_request_type(self):
return self.check_params_right(
RequestType,
self.case_data.get(TestCaseEnum.REQUEST_TYPE.value[0])
)
@property
def get_dependence_case_data(self):
_dep_data = self.case_data.get(TestCaseEnum.DE_CASE.value[0])
if _dep_data:
assert self.case_data.get(TestCaseEnum.DE_CASE_DATA.value[0]) is not None, (
f"程序中检测到您的 case_id 为 {self.case_id} 的用例存在依赖,但是 {_dep_data} 缺少依赖数据."
f"如已填写,请检查缩进是否正确, 用例路径: {self.file_path}"
)
return self.case_data.get(TestCaseEnum.DE_CASE_DATA.value[0])
@property
def get_assert(self):
_assert_data = self.case_data.get(TestCaseEnum.ASSERT_DATA.value[0])
assert _assert_data is not None, (
f"用例ID 为 {self.case_id} 未添加断言,用例路径: {self.file_path}"
)
return _assert_data
@property
def get_sql(self):
_sql = self.case_data.get(TestCaseEnum.SQL.value[0])
# 判断数据库开关为开启状态,并且sql不为空
if config.mysql_db.switch and _sql is None:
return None
return _sql
class CaseData(CaseDataCheck):
def case_process(self, case_id_switch: Union[None, bool] = None):
data = GetYamlData(self.file_path).get_yaml_data()
case_list = []
for key, values in data.items():
# 公共配置中的数据,与用例数据不同,需要单独处理
if key != 'case_common':
self.case_data = values
self.case_id = key
super().check_params_exit()
case_date = {
'method': self.get_method,
'is_run': self.case_data.get(TestCaseEnum.IS_RUN.value[0]),
'url': self.get_host,
'detail': self.case_data.get(TestCaseEnum.DETAIL.value[0]),
'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]),
'requestType': super().get_request_type,
'data': self.case_data.get(TestCaseEnum.DATA.value[0]),
'dependence_case': self.case_data.get(TestCaseEnum.DE_CASE.value[0]),
'dependence_case_data': self.get_dependence_case_data,
"current_request_set_cache": self.case_data.get(TestCaseEnum.CURRENT_RE_SET_CACHE.value[0]),
"sql": self.get_sql,
"assert_data": self.get_assert,
"setup_sql": self.case_data.get(TestCaseEnum.SETUP_SQL.value[0]),
"teardown": self.case_data.get(TestCaseEnum.TEARDOWN.value[0]),
"teardown_sql": self.case_data.get(TestCaseEnum.TEARDOWN_SQL.value[0]),
"sleep": self.case_data.get(TestCaseEnum.SLEEP.value[0]),
}
if case_id_switch is True:
case_list.append({key: TestCase(**case_date).dict()})
else:
case_list.append(TestCase(**case_date).dict())
return case_list
class GetTestCase:
@staticmethod
def case_data(case_id_lists: List):
case_lists = []
for i in case_id_lists:
_data = CacheHandler.get_cache(i)
case_lists.append(_data)
return case_lists
"""
Desc : 自定义函数调用
"""
import re
import datetime
import random
from datetime import date, timedelta, datetime
from jsonpath import jsonpath
from faker import Faker
from utils.logging_tool.log_control import ERROR
class Context:
""" 正则替换 """
def __init__(self):
self.faker = Faker(locale='zh_CN')
@classmethod
def random_int(cls) -> int:
"""
:return: 随机数
"""
_data = random.randint(0, 5000)
return _data
def get_phone(self) -> int:
"""
:return: 随机生成手机号码
"""
phone = self.faker.phone_number()
return phone
def get_id_number(self) -> int:
"""
:return: 随机生成身份证号码
"""
id_number = self.faker.ssn()
return id_number
def get_female_name(self) -> str:
"""
:return: 女生姓名
"""
female_name = self.faker.name_female()
return female_name
def get_male_name(self) -> str:
"""
:return: 男生姓名
"""
male_name = self.faker.name_male()
return male_name
def get_email(self) -> str:
"""
:return: 生成邮箱
"""
email = self.faker.email()
return email
@classmethod
def self_operated_id(cls):
"""自营店铺 ID """
operated_id = 212
return operated_id
@classmethod
def get_time(cls) -> str:
"""
计算当前时间
:return:
"""
now_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
return now_time
@classmethod
def today_date(cls):
"""获取今日0点整时间"""
_today = date.today().strftime("%Y-%m-%d") + " 00:00:00"
return str(_today)
@classmethod
def time_after_week(cls):
"""获取一周后12点整的时间"""
_time_after_week = (date.today() + timedelta(days=+6)).strftime("%Y-%m-%d") + " 00:00:00"
return _time_after_week
@classmethod
def host(cls) -> str:
from utils import config
""" 获取接口域名 """
return config.host
@classmethod
def app_host(cls) -> str:
from utils import config
"""获取app的host"""
return config.app_host
def sql_json(js_path, res):
""" 提取 sql中的 json 数据 """
_json_data = jsonpath(res, js_path)[0]
if _json_data is False:
raise ValueError(f"sql中的jsonpath获取失败 {res}, {js_path}")
return jsonpath(res, js_path)[0]
def sql_regular(value, res=None):
"""
这里处理sql中的依赖数据,通过获取接口响应的jsonpath的值进行替换
:param res: jsonpath使用的返回结果
:param value:
:return:
"""
sql_json_list = re.findall(r"\$json\((.*?)\)\$", value)
for i in sql_json_list:
pattern = re.compile(r'\$json\(' + i.replace('$', "\$").replace('[', '\[') + r'\)\$')
key = str(sql_json(i, res))
value = re.sub(pattern, key, value, count=1)
return value
def cache_regular(value):
from utils.cache_process.cache_control import CacheHandler
"""
通过正则的方式,读取缓存中的内容
例:$cache{login_init}
:param value:
:return:
"""
# 正则获取 $cache{login_init}中的值 --> login_init
regular_dates = re.findall(r"\$cache\{(.*?)\}", value)
# 拿到的是一个list,循环数据
for regular_data in regular_dates:
value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
if any(i in regular_data for i in value_types) is True:
value_types = regular_data.split(":")[0]
regular_data = regular_data.split(":")[1]
# pattern = re.compile(r'\'\$cache{' + value_types.split(":")[0] + r'(.*?)}\'')
pattern = re.compile(r'\'\$cache\{' + value_types.split(":")[0] + ":" + regular_data + r'\}\'')
else:
pattern = re.compile(
r'\$cache\{' + regular_data.replace('$', "\$").replace('[', '\[') + r'\}'
)
try:
# cache_data = Cache(regular_data).get_cache()
cache_data = CacheHandler.get_cache(regular_data)
# 使用sub方法,替换已经拿到的内容
value = re.sub(pattern, str(cache_data), value)
except Exception:
pass
return value
def regular(target):
"""
新版本
使用正则替换请求数据
:return:
"""
try:
regular_pattern = r'\${{(.*?)}}'
while re.findall(regular_pattern, target):
key = re.search(regular_pattern, target).group(1)
value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
if any(i in key for i in value_types) is True:
func_name = key.split(":")[1].split("(")[0]
value_name = key.split(":")[1].split("(")[1][:-1]
if value_name == "":
value_data = getattr(Context(), func_name)()
else:
value_data = getattr(Context(), func_name)(*value_name.split(","))
regular_int_pattern = r'\'\${{(.*?)}}\''
target = re.sub(regular_int_pattern, str(value_data), target, 1)
else:
func_name = key.split("(")[0]
value_name = key.split("(")[1][:-1]
if value_name == "":
value_data = getattr(Context(), func_name)()
else:
value_data = getattr(Context(), func_name)(*value_name.split(","))
target = re.sub(regular_pattern, str(value_data), target, 1)
return target
except AttributeError:
ERROR.logger.error("未找到对应的替换的数据, 请检查数据是否正确 %s", target)
raise
except IndexError:
ERROR.logger.error("yaml中的 ${{}} 函数方法不正确,正确语法实例:${{get_time()}}")
raise
if __name__ == '__main__':
a = "${{host()}} aaa"
b = regular(a)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import json
from jsonpath import jsonpath
from common.setting import ensure_path_sep
from typing import Dict
from ruamel import yaml
import os
class SwaggerForYaml:
def __init__(self):
self._data = self.get_swagger_json()
@classmethod
def get_swagger_json(cls):
"""
获取 swagger 中的 json 数据
:return:
"""
try:
with open('./file/test_OpenAPI.json', "r", encoding='utf-8') as f:
row_data = json.load(f)
return row_data
except FileNotFoundError:
raise FileNotFoundError("文件路径不存在,请重新输入")
def get_allure_epic(self):
""" 获取 yaml 用例中的 allure_epic """
_allure_epic = self._data['info']['title']
return _allure_epic
@classmethod
def get_allure_feature(cls, value):
""" 获取 yaml 用例中的 allure_feature """
_allure_feature = value['tags']
return str(_allure_feature)
@classmethod
def get_allure_story(cls, value):
""" 获取 yaml 用例中的 allure_story """
_allure_story = value['summary']
return _allure_story
@classmethod
def get_case_id(cls, value):
""" 获取 case_id """
_case_id = value.replace("/", "_")
return "01" + _case_id
@classmethod
def get_detail(cls, value):
_get_detail = value['summary']
return "测试" + _get_detail
@classmethod
def get_request_type(cls, value, headers):
""" 处理 request_type """
if jsonpath(obj=value, expr="$.parameters") is not False:
_parameters = value['parameters']
if _parameters[0]['in'] == 'query':
return "params"
else:
if 'application/x-www-form-urlencoded' or 'multipart/form-data' in headers:
return "data"
elif 'application/json' in headers:
return "json"
elif 'application/octet-stream' in headers:
return "file"
else:
return "data"
@classmethod
def get_case_data(cls, value):
""" 处理 data 数据 """
_dict = {}
if jsonpath(obj=value, expr="$.parameters") is not False:
_parameters = value['parameters']
for i in _parameters:
if i['in'] == 'header':
...
else:
_dict[i['name']] = None
else:
return None
return _dict
@classmethod
def yaml_cases(cls, data: Dict, file_path: str) -> None:
"""
写入 yaml 数据
:param file_path:
:param data: 测试用例数据
:return:
"""
_file_path = ensure_path_sep("\\data\\" + file_path[1:].replace("/", os.sep) + '.yaml')
_file = _file_path.split(os.sep)[:-1]
_dir_path = ''
for i in _file:
_dir_path += i + os.sep
try:
os.makedirs(_dir_path)
except FileExistsError:
...
with open(_file_path, "a", encoding="utf-8") as file:
yaml.dump(data, file, Dumper=yaml.RoundTripDumper, allow_unicode=True)
file.write('\n')
@classmethod
def get_headers(cls, value):
""" 获取请求头 """
_headers = {}
if jsonpath(obj=value, expr="$.consumes") is not False:
_headers = {"Content-Type": value['consumes'][0]}
if jsonpath(obj=value, expr="$.parameters") is not False:
for i in value['parameters']:
if i['in'] == 'header':
_headers[i['name']] = None
else:
_headers = None
return _headers
def write_yaml_handler(self):
_api_data = self._data['paths']
for key, value in _api_data.items():
for k, v in value.items():
yaml_data = {
"case_common": {"allureEpic": self.get_allure_epic(), "allureFeature": self.get_allure_feature(v),
"allureStory": self.get_allure_story(v)},
self.get_case_id(key): {
"host": "${{host()}}", "url": key, "method": k, "detail": self.get_detail(v),
"headers": self.get_headers(v), "requestType": self.get_request_type(v, self.get_headers(v)),
"is_run": None, "data": self.get_case_data(v), "dependence_case": False,
"assert": {"status_code": 200}, "sql": None}}
self.yaml_cases(yaml_data, file_path=key)
if __name__ == '__main__':
SwaggerForYaml().write_yaml_handler()
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
# @describe: 用例模板
"""
import datetime
import os
from utils.read_files_tools.yaml_control import GetYamlData
from common.setting import ensure_path_sep
from utils.other_tools.exceptions import ValueNotFoundError
def write_case(case_path, page):
""" 写入用例数据 """
with open(case_path, 'w', encoding="utf-8") as file:
file.write(page)
def write_testcase_file(*, allure_epic, allure_feature, class_title,
func_title, case_path, case_ids, file_name, allure_story):
"""
:param allure_story:
:param file_name: 文件名称
:param allure_epic: 项目名称
:param allure_feature: 模块名称
:param class_title: 类名称
:param func_title: 函数名称
:param case_path: case 路径
:param case_ids: 用例ID
:return:
"""
conf_data = GetYamlData(ensure_path_sep("\\common\\config.yaml")).get_yaml_data()
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
real_time_update_test_cases = conf_data['real_time_update_test_cases']
page = f'''#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : {now}
import allure
import pytest
from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
from utils.assertion.assert_control import Assert
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import regular
from utils.requests_tool.teardown_control import TearDownHandler
case_id = {case_ids}
TestData = GetTestCase.case_data(case_id)
re_data = regular(str(TestData))
@allure.epic("{allure_epic}")
@allure.feature("{allure_feature}")
class Test{class_title}:
@allure.story("{allure_story}")
@pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
def test_{func_title}(self, in_data, case_skip):
"""
:param :
:return:
"""
res = RequestControl(in_data).http_request()
TearDownHandler(res).teardown_handle()
Assert(assert_data=in_data['assert_data'],
sql_data=res.sql_data,
request_data=res.body,
response_data=res.response_data,
status_code=res.status_code).assert_type_handle()
if __name__ == '__main__':
pytest.main(['{file_name}', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
'''
if real_time_update_test_cases:
write_case(case_path=case_path, page=page)
elif real_time_update_test_cases is False:
if not os.path.exists(case_path):
write_case(case_path=case_path, page=page)
else:
raise ValueNotFoundError("real_time_update_test_cases 配置不正确,只能配置 True 或者 False")
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import os
import ast
import yaml.scanner
from utils.read_files_tools.regular_control import regular
class GetYamlData:
""" 获取 yaml 文件中的数据 """
def __init__(self, file_dir):
self.file_dir = str(file_dir)
def get_yaml_data(self) -> dict:
"""
获取 yaml 中的数据
:param: fileDir:
:return:
"""
# 判断文件是否存在
if os.path.exists(self.file_dir):
data = open(self.file_dir, 'r', encoding='utf-8')
res = yaml.load(data, Loader=yaml.FullLoader)
else:
raise FileNotFoundError("文件路径不存在")
return res
def write_yaml_data(self, key: str, value) -> int:
"""
更改 yaml 文件中的值, 并且保留注释内容
:param key: 字典的key
:param value: 写入的值
:return:
"""
with open(self.file_dir, 'r', encoding='utf-8') as file:
# 创建了一个空列表,里面没有元素
lines = []
for line in file.readlines():
if line != '\n':
lines.append(line)
file.close()
with open(self.file_dir, 'w', encoding='utf-8') as file:
flag = 0
for line in lines:
left_str = line.split(":")[0]
if key == left_str.lstrip() and '#' not in line:
newline = f"{left_str}: {value}"
line = newline
file.write(f'{line}\n')
flag = 1
else:
file.write(f'{line}')
file.close()
return flag
class GetCaseData(GetYamlData):
""" 获取测试用例中的数据 """
def get_different_formats_yaml_data(self) -> list:
"""
获取兼容不同格式的yaml数据
:return:
"""
res_list = []
for i in self.get_yaml_data():
res_list.append(i)
return res_list
def get_yaml_case_data(self):
"""
获取测试用例数据, 转换成指定数据格式
:return:
"""
_yaml_data = self.get_yaml_data()
# 正则处理yaml文件中的数据
re_data = regular(str(_yaml_data))
return ast.literal_eval(re_data)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
from urllib.parse import parse_qs, urlparse
from typing import Any, Union, Text, List, Dict, Tuple
import ast
import os
import mitmproxy.http
from mitmproxy import ctx
from ruamel import yaml
class Counter:
"""
代理录制,基于 mitmproxy 库拦截获取网络请求
将接口请求数据转换成 yaml 测试用例
参考资料: https://blog.wolfogre.com/posts/usage-of-mitmproxy/
"""
def __init__(self, filter_url: List, filename: Text = './data/proxy_data.yaml'):
self.num = 0
self.file = filename
self.counter = 1
# 需要过滤的 url
self.url = filter_url
def response(self, flow: mitmproxy.http.HTTPFlow) -> None:
"""
mitmproxy抓包处理响应,在这里汇总需要数据, 过滤 包含指定url,并且响应格式是 json的
:param flow:
:return:
"""
# 存放需要过滤的接口
filter_url_type = ['.css', '.js', '.map', '.ico', '.png', '.woff', '.map3', '.jpeg', '.jpg']
url = flow.request.url
ctx.log.info("=" * 100)
# 判断过滤掉含 filter_url_type 中后缀的 url
if any(i in url for i in filter_url_type) is False:
# 存放测试用例
if self.filter_url(url):
data = self.data_handle(flow.request.text)
method = flow.request.method
header = self.token_handle(flow.request.headers)
response = flow.response.text
case_id = self.get_case_id(url) + str(self.counter)
cases = {
case_id: {
"host": self.host_handle(url),
"url": self.url_path_handle(url),
"method": method,
"detail": None,
"headers": header,
'requestType': self.request_type_handler(method),
"is_run": True,
"data": data,
"dependence_case": None,
"dependence_case_data": None,
"assert": self.response_code_handler(response),
"sql": None
}
}
# 判断如果请求参数时拼接在url中,提取url中参数,转换成字典
if "?" in url:
cases[case_id]['url'] = self.get_url_handler(url)[1]
cases[case_id]['data'] = self.get_url_handler(url)[0]
ctx.log.info("=" * 100)
ctx.log.info(cases)
# 判断文件不存在则创建文件
try:
self.yaml_cases(cases)
except FileNotFoundError:
os.makedirs(self.file)
self.counter += 1
@classmethod
def get_case_id(cls, url: Text) -> Text:
"""
通过url,提取对应的user_id
:param url:
:return:
"""
_url_path = str(url).split('?')[0]
# 通过url中的接口地址,最后一个参数,作为case_id的名称
_url = _url_path.split('/')
return _url[-1]
def filter_url(self, url: Text) -> bool:
"""过滤url"""
for i in self.url:
# 判断当前拦截的url地址,是否是addons中配置的host
if i in url:
# 如果是,则返回True
return True
# 否则返回 False
return False
@classmethod
def response_code_handler(cls, response) -> Union[Dict, None]:
"""
处理接口响应,默认断言数据为code码,如果接口没有code码,则返回None
@param response:
@return:
"""
try:
data = cls.data_handle(response)
return {"code": {"jsonpath": "$.code", "type": "==",
"value": data['code'], "AssertType": None}}
except KeyError:
return None
except NameError:
return None
@classmethod
def request_type_handler(cls, method: Text) -> Text:
""" 处理请求类型,有params、json、file,需要根据公司的业务情况自己调整 """
if method == 'GET':
# 如我们公司只有get请求是prams,其他都是json的
return 'params'
return 'json'
@classmethod
def data_handle(cls, dict_str) -> Any:
"""处理接口请求、响应的数据,如null、true格式问题"""
try:
if dict_str != "":
if 'null' in dict_str:
dict_str = dict_str.replace('null', 'None')
if 'true' in dict_str:
dict_str = dict_str.replace('true', 'True')
if 'false' in dict_str:
dict_str = dict_str.replace('false', 'False')
dict_str = ast.literal_eval(dict_str)
if dict_str == "":
dict_str = None
return dict_str
except Exception as exc:
raise exc
@classmethod
def token_handle(cls, header) -> Dict:
"""
提取请求头参数
:param header:
:return:
"""
# 这里是将所有请求头的数据,全部都拦截出来了
# 如果公司只需要部分参数,可以在这里加判断过滤
headers = {}
for key, value in header.items():
headers[key] = value
return headers
def host_handle(self, url: Text) -> Tuple:
"""
解析 url
:param url: https://xxxx.test.xxxx.com/#/goods/listShop
:return: https://xxxx.test.xxxx.com/
"""
host = None
# 循环遍历需要过滤的hosts数据
for i in self.url:
# 这里主要是判断,如果我们conf.py中有配置这个域名,则用例中展示 ”${{host}}“,动态获取用例host
# 大家可以在这里改成自己公司的host地址
if 'https://www.wanandroid.com' in url:
host = '${{host}}'
elif i in url:
host = i
return host
def url_path_handle(self, url: Text):
"""
解析 url_path
:param url: https://xxxx.test.xxxx.com/shopList/json
:return: /shopList/json
"""
url_path = None
# 循环需要拦截的域名
for path in self.url:
if path in url:
url_path = url.split(path)[-1]
return url_path
def yaml_cases(self, data: Dict) -> None:
"""
写入 yaml 数据
:param data: 测试用例数据
:return:
"""
with open(self.file, "a", encoding="utf-8") as file:
yaml.dump(data, file, Dumper=yaml.RoundTripDumper, allow_unicode=True)
file.write('\n')
def get_url_handler(self, url: Text) -> Tuple:
"""
将 url 中的参数 转换成字典
:param url: /trade?tradeNo=&outTradeId=11
:return: {“outTradeId”: 11}
"""
result = None
url_path = None
for i in self.url:
if i in url:
query = urlparse(url).query
# 将字符串转换为字典
params = parse_qs(query)
# 所得的字典的value都是以列表的形式存在,如请求url中的参数值为空,则字典中不会有该参数
result = {key: params[key][0] for key in params}
url = url[0:url.rfind('?')]
url_path = url.split(i)[-1]
return result, url_path
# 1、本机需要设置代理,默认端口为: 8080
# 2、控制台输入 mitmweb -s .\utils\recording\mitmproxy_control.py - p 8888命令开启代理模式进行录制
addons = [
Counter(["https://www.wanandroid.com"])
]
"""
"""
import ast
import json
from typing import Text, Dict, Union, List
from jsonpath import jsonpath
from utils.requests_tool.request_control import RequestControl
from utils.mysql_tool.mysql_control import SetUpMySQL
from utils.read_files_tools.regular_control import regular, cache_regular
from utils.other_tools.jsonpath_date_replace import jsonpath_replace
from utils.logging_tool.log_control import WARNING
from utils.other_tools.models import DependentType
from utils.other_tools.models import TestCase, DependentCaseData, DependentData
from utils.other_tools.exceptions import ValueNotFoundError
from utils.cache_process.cache_control import CacheHandler
from utils import config
class DependentCase:
""" 处理依赖相关的业务 """
def __init__(self, dependent_yaml_case: TestCase):
self.__yaml_case = dependent_yaml_case
@classmethod
def get_cache(cls, case_id: Text) -> Dict:
"""
获取缓存用例池中的数据,通过 case_id 提取
:param case_id:
:return: case_id_01
"""
_case_data = CacheHandler.get_cache(case_id)
return _case_data
@classmethod
def jsonpath_data(
cls,
obj: Dict,
expr: Text) -> list:
"""
通过jsonpath提取依赖的数据
:param obj: 对象信息
:param expr: jsonpath 方法
:return: 提取到的内容值,返回是个数组
对象: {"data": applyID} --> jsonpath提取方法: $.data.data.[0].applyId
"""
_jsonpath_data = jsonpath(obj, expr)
# 判断是否正常提取到数据,如未提取到,则抛异常
if _jsonpath_data is False:
raise ValueNotFoundError(
f"jsonpath提取失败!\n 提取的数据: {obj} \n jsonpath规则: {expr}"
)
return _jsonpath_data
@classmethod
def set_cache_value(cls, dependent_data: "DependentData") -> Union[Text, None]:
"""
获取依赖中是否需要将数据存入缓存中
"""
try:
return dependent_data.set_cache
except KeyError:
return None
@classmethod
def replace_key(cls, dependent_data: "DependentData"):
""" 获取需要替换的内容 """
try:
_replace_key = dependent_data.replace_key
return _replace_key
except KeyError:
return None
def url_replace(
self,
replace_key: Text,
jsonpath_dates: Dict,
jsonpath_data: list) -> None:
"""
url中的动态参数替换
# 如: 一般有些接口的参数在url中,并且没有参数名称, /api/v1/work/spu/approval/spuApplyDetails/{id}
# 那么可以使用如下方式编写用例, 可以使用 $url_params{}替换,
# 如/api/v1/work/spu/approval/spuApplyDetails/$url_params{id}
:param jsonpath_data: jsonpath 解析出来的数据值
:param replace_key: 用例中需要替换数据的 replace_key
:param jsonpath_dates: jsonpath 存放的数据值
:return:
"""
if "$url_param" in replace_key:
_url = self.__yaml_case.url.replace(replace_key, str(jsonpath_data[0]))
jsonpath_dates['$.url'] = _url
else:
jsonpath_dates[replace_key] = jsonpath_data[0]
def _dependent_type_for_sql(
self,
setup_sql: List,
dependence_case_data: "DependentCaseData",
jsonpath_dates: Dict) -> None:
"""
判断依赖类型为 sql,程序中的依赖参数从 数据库中提取数据
@param setup_sql: 前置sql语句
@param dependence_case_data: 依赖的数据
@param jsonpath_dates: 依赖相关的用例数据
@return:
"""
# 判断依赖数据类型,依赖 sql中的数据
if setup_sql is not None:
if config.mysql_db.switch:
setup_sql = ast.literal_eval(cache_regular(str(setup_sql)))
sql_data = SetUpMySQL().setup_sql_data(sql=setup_sql)
dependent_data = dependence_case_data.dependent_data
for i in dependent_data:
_jsonpath = i.jsonpath
jsonpath_data = self.jsonpath_data(obj=sql_data, expr=_jsonpath)
_set_value = self.set_cache_value(i)
_replace_key = self.replace_key(i)
if _set_value is not None:
CacheHandler.update_cache(cache_name=_set_value, value=jsonpath_data[0])
# Cache(_set_value).set_caches(jsonpath_data[0])
if _replace_key is not None:
jsonpath_dates[_replace_key] = jsonpath_data[0]
self.url_replace(
replace_key=_replace_key,
jsonpath_dates=jsonpath_dates,
jsonpath_data=jsonpath_data,
)
else:
WARNING.logger.warning("检查到数据库开关为关闭状态,请确认配置")
def dependent_handler(
self,
_jsonpath: Text,
set_value: Text,
replace_key: Text,
jsonpath_dates: Dict,
data: Dict,
dependent_type: int
) -> None:
""" 处理数据替换 """
jsonpath_data = self.jsonpath_data(
data,
_jsonpath
)
if set_value is not None:
if len(jsonpath_data) > 1:
CacheHandler.update_cache(cache_name=set_value, value=jsonpath_data)
else:
CacheHandler.update_cache(cache_name=set_value, value=jsonpath_data[0])
if replace_key is not None:
if dependent_type == 0:
jsonpath_dates[replace_key] = jsonpath_data[0]
self.url_replace(replace_key=replace_key, jsonpath_dates=jsonpath_dates,
jsonpath_data=jsonpath_data)
def is_dependent(self) -> Union[Dict, bool]:
"""
判断是否有数据依赖
:return:
"""
# 获取用例中的dependent_type值,判断该用例是否需要执行依赖
_dependent_type = self.__yaml_case.dependence_case
# 获取依赖用例数据
_dependence_case_dates = self.__yaml_case.dependence_case_data
_setup_sql = self.__yaml_case.setup_sql
# 判断是否有依赖
if _dependent_type is True:
# 读取依赖相关的用例数据
jsonpath_dates = {}
# 循环所有需要依赖的数据
try:
for dependence_case_data in _dependence_case_dates:
_case_id = dependence_case_data.case_id
# 判断依赖数据为sql,case_id需要写成self,否则程序中无法获取case_id
if _case_id == 'self':
self._dependent_type_for_sql(
setup_sql=_setup_sql,
dependence_case_data=dependence_case_data,
jsonpath_dates=jsonpath_dates)
else:
re_data = regular(str(self.get_cache(_case_id)))
re_data = ast.literal_eval(cache_regular(str(re_data)))
res = RequestControl(re_data).http_request()
if dependence_case_data.dependent_data is not None:
dependent_data = dependence_case_data.dependent_data
for i in dependent_data:
_case_id = dependence_case_data.case_id
_jsonpath = i.jsonpath
_request_data = self.__yaml_case.data
_replace_key = self.replace_key(i)
_set_value = self.set_cache_value(i)
# 判断依赖数据类型, 依赖 response 中的数据
if i.dependent_type == DependentType.RESPONSE.value:
self.dependent_handler(
data=json.loads(res.response_data),
_jsonpath=_jsonpath,
set_value=_set_value,
replace_key=_replace_key,
jsonpath_dates=jsonpath_dates,
dependent_type=0
)
# 判断依赖数据类型, 依赖 request 中的数据
elif i.dependent_type == DependentType.REQUEST.value:
self.dependent_handler(
data=res.body,
_jsonpath=_jsonpath,
set_value=_set_value,
replace_key=_replace_key,
jsonpath_dates=jsonpath_dates,
dependent_type=1
)
else:
raise ValueError(
"依赖的dependent_type不正确,只支持request、response、sql依赖\n"
f"当前填写内容: {i.dependent_type}"
)
return jsonpath_dates
except KeyError as exc:
# pass
raise ValueNotFoundError(
f"dependence_case_data依赖用例中,未找到 {exc} 参数,请检查是否填写"
f"如已填写,请检查是否存在yaml缩进问题"
) from exc
except TypeError as exc:
raise ValueNotFoundError(
"dependence_case_data下的所有内容均不能为空!"
"请检查相关数据是否填写,如已填写,请检查缩进问题"
) from exc
else:
return False
def get_dependent_data(self) -> None:
"""
jsonpath 和 依赖的数据,进行替换
:return:
"""
_dependent_data = DependentCase(self.__yaml_case).is_dependent()
_new_data = None
# 判断有依赖
if _dependent_data is not None and _dependent_data is not False:
# if _dependent_data is not False:
for key, value in _dependent_data.items():
# 通过jsonpath判断出需要替换数据的位置
_change_data = key.split(".")
# jsonpath 数据解析
# 不要删 这个yaml_case
yaml_case = self.__yaml_case
_new_data = jsonpath_replace(change_data=_change_data, key_name='yaml_case')
# 最终提取到的数据,转换成 __yaml_case.data
_new_data += ' = ' + str(value)
exec(_new_data)
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
"""
import hashlib
from hashlib import sha256
import hmac
from typing import Text
import binascii
from pyDes import des, ECB, PAD_PKCS5
def hmac_sha256_encrypt(key, data):
"""hmac sha 256算法"""
_key = key.encode('utf8')
_data = data.encode('utf8')
encrypt_data = hmac.new(_key, _data, digestmod=sha256).hexdigest()
return encrypt_data
def md5_encryption(value):
""" md5 加密"""
str_md5 = hashlib.md5(str(value).encode(encoding='utf-8')).hexdigest()
return str_md5
def sha1_secret_str(_str: Text):
"""
使用sha1加密算法,返回str加密后的字符串
"""
encrypts = hashlib.sha1(_str.encode('utf-8')).hexdigest()
return encrypts
def des_encrypt(_str):
"""
DES 加密
:return: 加密后字符串,16进制
"""
# 密钥,自行修改
_key = 'PASSWORD'
secret_key = _key
_iv = secret_key
key = des(secret_key, ECB, _iv, pad=None, padmode=PAD_PKCS5)
_encrypt = key.encrypt(_str, padmode=PAD_PKCS5)
return binascii.b2a_hex(_encrypt)
def encryption(ency_type):
"""
:param ency_type: 加密类型
:return:
"""
def decorator(func):
def swapper(*args, **kwargs):
res = func(*args, **kwargs)
_data = res['body']
if ency_type == "md5":
def ency_value(data):
if data is not None:
for key, value in data.items():
if isinstance(value, dict):
ency_value(data=value)
else:
data[key] = md5_encryption(value)
else:
raise ValueError("暂不支持该加密规则,如有需要,请联系管理员")
ency_value(_data)
return res
return swapper
return decorator
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import ast
import os
import random
import time
import urllib
from typing import Tuple, Dict, Union, Text
import requests
import urllib3
from requests_toolbelt import MultipartEncoder
from common.setting import ensure_path_sep
from utils.other_tools.models import RequestType
from utils.logging_tool.log_decorator import log_decorator
from utils.mysql_tool.mysql_control import AssertExecution
from utils.logging_tool.run_time_decorator import execution_duration
from utils.other_tools.allure_data.allure_tools import allure_step, allure_step_no, allure_attach
from utils.read_files_tools.regular_control import cache_regular
from utils.requests_tool.set_current_request_cache import SetCurrentRequestCache
from utils.other_tools.models import TestCase, ResponseData
from utils import config
# from utils.requests_tool.encryption_algorithm_control import encryption
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class RequestControl:
""" 封装请求 """
def __init__(self, yaml_case):
self.__yaml_case = TestCase(**yaml_case)
def file_data_exit(
self,
file_data) -> None:
"""判断上传文件时,data参数是否存在"""
# 兼容又要上传文件,又要上传其他类型参数
try:
_data = self.__yaml_case.data
for key, value in ast.literal_eval(cache_regular(str(_data)))['data'].items():
if "multipart/form-data" in str(self.__yaml_case.headers.values()):
file_data[key] = str(value)
else:
file_data[key] = value
except KeyError:
...
@classmethod
def multipart_data(
cls,
file_data: Dict):
""" 处理上传文件数据 """
multipart = MultipartEncoder(
fields=file_data, # 字典格式
boundary='-----------------------------' + str(random.randint(int(1e28), int(1e29 - 1)))
)
return multipart
@classmethod
def check_headers_str_null(
cls,
headers: Dict) -> Dict:
"""
兼容用户未填写headers或者header值为int
@return:
"""
headers = ast.literal_eval(cache_regular(str(headers)))
if headers is None:
headers = {"headers": None}
else:
for key, value in headers.items():
if not isinstance(value, str):
headers[key] = str(value)
return headers
@classmethod
def multipart_in_headers(
cls,
request_data: Dict,
header: Dict):
""" 判断处理header为 Content-Type: multipart/form-data"""
header = ast.literal_eval(cache_regular(str(header)))
request_data = ast.literal_eval(cache_regular(str(request_data)))
if header is None:
header = {"headers": None}
else:
# 将header中的int转换成str
for key, value in header.items():
if not isinstance(value, str):
header[key] = str(value)
if "multipart/form-data" in str(header.values()):
# 判断请求参数不为空, 并且参数是字典类型
if request_data and isinstance(request_data, dict):
# 当 Content-Type 为 "multipart/form-data"时,需要将数据类型转换成 str
for key, value in request_data.items():
if not isinstance(value, str):
request_data[key] = str(value)
request_data = MultipartEncoder(request_data)
header['Content-Type'] = request_data.content_type
return request_data, header
def file_prams_exit(self) -> Dict:
"""判断上传文件接口,文件参数是否存在"""
try:
params = self.__yaml_case.data['params']
except KeyError:
params = None
return params
@classmethod
def text_encode(
cls,
text: Text) -> Text:
"""unicode 解码"""
return text.encode("utf-8").decode("utf-8")
@classmethod
def response_elapsed_total_seconds(
cls,
res) -> float:
"""获取接口响应时长"""
try:
return round(res.elapsed.total_seconds() * 1000, 2)
except AttributeError:
return 0.00
def upload_file(
self) -> Tuple:
"""
判断处理上传文件
:return:
"""
# 处理上传多个文件的情况
_files = []
file_data = {}
# 兼容又要上传文件,又要上传其他类型参数
self.file_data_exit(file_data)
_data = self.__yaml_case.data
for key, value in ast.literal_eval(cache_regular(str(_data)))['file'].items():
file_path = ensure_path_sep("\\Files\\" + value)
file_data[key] = (value, open(file_path, 'rb'), 'application/octet-stream')
_files.append(file_data)
# allure中展示该附件
allure_attach(source=file_path, name=value, extension=value)
multipart = self.multipart_data(file_data)
# ast.literal_eval(cache_regular(str(_headers)))['Content-Type'] = multipart.content_type
self.__yaml_case.headers['Content-Type'] = multipart.content_type
params_data = ast.literal_eval(cache_regular(str(self.file_prams_exit())))
return multipart, params_data, self.__yaml_case
def request_type_for_json(
self,
headers: Dict,
method: Text,
**kwargs):
""" 判断请求类型为json格式 """
_headers = self.check_headers_str_null(headers)
_data = self.__yaml_case.data
_url = self.__yaml_case.url
res = requests.request(
method=method,
url=cache_regular(str(_url)),
json=ast.literal_eval(cache_regular(str(_data))),
data={},
headers=_headers,
verify=False,
params=None,
**kwargs
)
return res
def request_type_for_none(
self,
headers: Dict,
method: Text,
**kwargs) -> object:
"""判断 requestType 为 None"""
_headers = self.check_headers_str_null(headers)
_url = self.__yaml_case.url
res = requests.request(
method=method,
url=cache_regular(_url),
data=None,
headers=_headers,
verify=False,
params=None,
**kwargs
)
return res
def request_type_for_params(
self,
headers: Dict,
method: Text,
**kwargs):
"""处理 requestType 为 params """
_data = self.__yaml_case.data
url = self.__yaml_case.url
if _data is not None:
# url 拼接的方式传参
params_data = "?"
for key, value in _data.items():
if value is None or value == '':
params_data += (key + "&")
else:
params_data += (key + "=" + str(value) + "&")
url = self.__yaml_case.url + params_data[:-1]
_headers = self.check_headers_str_null(headers)
res = requests.request(
method=method,
url=cache_regular(url),
headers=_headers,
verify=False,
data={},
params=None,
**kwargs)
return res
def request_type_for_file(
self,
method: Text,
headers,
**kwargs):
"""处理 requestType 为 file 类型"""
multipart = self.upload_file()
yaml_data = multipart[2]
_headers = multipart[2].headers
_headers = self.check_headers_str_null(_headers)
res = requests.request(
method=method,
url=cache_regular(yaml_data.url),
data=multipart[0],
params=multipart[1],
headers=ast.literal_eval(cache_regular(str(_headers))),
verify=False,
**kwargs
)
return res
def request_type_for_data(
self,
headers: Dict,
method: Text,
**kwargs):
"""判断 requestType 为 data 类型"""
data = self.__yaml_case.data
_data, _headers = self.multipart_in_headers(
ast.literal_eval(cache_regular(str(data))),
headers
)
_url = self.__yaml_case.url
res = requests.request(
method=method,
url=cache_regular(_url),
data=_data,
headers=_headers,
verify=False,
**kwargs)
return res
@classmethod
def get_export_api_filename(cls, res):
""" 处理导出文件 """
content_disposition = res.headers.get('content-disposition')
filename_code = content_disposition.split("=")[-1] # 分隔字符串,提取文件名
filename = urllib.parse.unquote(filename_code) # url解码
return filename
def request_type_for_export(
self,
headers: Dict,
method: Text,
**kwargs):
"""判断 requestType 为 export 导出类型"""
_headers = self.check_headers_str_null(headers)
_data = self.__yaml_case.data
_url = self.__yaml_case.url
res = requests.request(
method=method,
url=cache_regular(_url),
json=ast.literal_eval(cache_regular(str(_data))),
headers=_headers,
verify=False,
stream=False,
data={},
**kwargs)
filepath = os.path.join(ensure_path_sep("\\Files\\"), self.get_export_api_filename(res)) # 拼接路径
if res.status_code == 200:
if res.text: # 判断文件内容是否为空
with open(filepath, 'wb') as file:
# iter_content循环读取信息写入,chunk_size设置文件大小
for chunk in res.iter_content(chunk_size=1):
file.write(chunk)
else:
print("文件为空")
return res
@classmethod
def _request_body_handler(cls, data: Dict, request_type: Text) -> Union[None, Dict]:
"""处理请求参数 """
if request_type.upper() == 'PARAMS':
return None
else:
return data
@classmethod
def _sql_data_handler(cls, sql_data, res):
"""处理 sql 参数 """
# 判断数据库开关,开启状态,则返回对应的数据
if config.mysql_db.switch and sql_data is not None:
sql_data = AssertExecution().assert_execution(
sql=sql_data,
resp=res.json()
)
else:
sql_data = {"sql": None}
return sql_data
def _check_params(
self,
res,
yaml_data: "TestCase",
) -> "ResponseData":
data = ast.literal_eval(cache_regular(str(yaml_data.data)))
_data = {
"url": res.url,
"is_run": yaml_data.is_run,
"detail": yaml_data.detail,
"response_data": res.text,
# 这个用于日志专用,判断如果是get请求,直接打印url
"request_body": self._request_body_handler(
data, yaml_data.requestType
),
"method": res.request.method,
"sql_data": self._sql_data_handler(sql_data=ast.literal_eval(cache_regular(str(yaml_data.sql))), res=res),
"yaml_data": yaml_data,
"headers": res.request.headers,
"cookie": res.cookies,
"assert_data": yaml_data.assert_data,
"res_time": self.response_elapsed_total_seconds(res),
"status_code": res.status_code,
"teardown": yaml_data.teardown,
"teardown_sql": yaml_data.teardown_sql,
"body": data
}
# 抽离出通用模块,判断 http_request 方法中的一些数据校验
return ResponseData(**_data)
@classmethod
def api_allure_step(
cls,
*,
url: Text,
headers: Text,
method: Text,
data: Text,
assert_data: Text,
res_time: Text,
res: Text
) -> None:
""" 在allure中记录请求数据 """
allure_step_no(f"请求URL: {url}")
allure_step_no(f"请求方式: {method}")
allure_step("请求头: ", headers)
allure_step("请求数据: ", data)
allure_step("预期数据: ", assert_data)
_res_time = res_time
allure_step_no(f"响应耗时(ms): {str(_res_time)}")
allure_step("响应结果: ", res)
@log_decorator(True)
@execution_duration(3000)
# @encryption("md5")
def http_request(
self,
dependent_switch=True,
**kwargs
):
"""
请求封装
:param dependent_switch:
:param kwargs:
:return:
"""
from utils.requests_tool.dependent_case import DependentCase
requests_type_mapping = {
RequestType.JSON.value: self.request_type_for_json,
RequestType.NONE.value: self.request_type_for_none,
RequestType.PARAMS.value: self.request_type_for_params,
RequestType.FILE.value: self.request_type_for_file,
RequestType.DATA.value: self.request_type_for_data,
RequestType.EXPORT.value: self.request_type_for_export
}
is_run = ast.literal_eval(cache_regular(str(self.__yaml_case.is_run)))
# 判断用例是否执行
if is_run is True or is_run is None:
# 处理多业务逻辑
if dependent_switch is True:
DependentCase(self.__yaml_case).get_dependent_data()
res = requests_type_mapping.get(self.__yaml_case.requestType)(
headers=self.__yaml_case.headers,
method=self.__yaml_case.method,
**kwargs
)
if self.__yaml_case.sleep is not None:
time.sleep(self.__yaml_case.sleep)
_res_data = self._check_params(
res=res,
yaml_data=self.__yaml_case)
self.api_allure_step(
url=_res_data.url,
headers=str(_res_data.headers),
method=_res_data.method,
data=str(_res_data.body),
assert_data=str(_res_data.assert_data),
res_time=str(_res_data.res_time),
res=_res_data.response_data
)
# 将当前请求数据存入缓存中
SetCurrentRequestCache(
current_request_set_cache=self.__yaml_case.current_request_set_cache,
request_data=self.__yaml_case.data,
response_data=res
).set_caches_main()
return _res_data
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
"""
import json
from typing import Text
from jsonpath import jsonpath
from utils.other_tools.exceptions import ValueNotFoundError
from utils.cache_process.cache_control import CacheHandler
class SetCurrentRequestCache:
"""将用例中的请求或者响应内容存入缓存"""
def __init__(
self,
current_request_set_cache,
request_data,
response_data
):
self.current_request_set_cache = current_request_set_cache
self.request_data = {"data": request_data}
self.response_data = response_data.text
def set_request_cache(
self,
jsonpath_value: Text,
cache_name: Text) -> None:
"""将接口的请求参数存入缓存"""
_request_data = jsonpath(
self.request_data,
jsonpath_value
)
if _request_data is not False:
CacheHandler.update_cache(cache_name=cache_name, value=_request_data[0])
# Cache(cache_name).set_caches(_request_data[0])
else:
raise ValueNotFoundError(
"缓存设置失败,程序中未检测到需要缓存的数据。"
f"请求参数: {self.request_data}"
f"提取的 jsonpath 内容: {jsonpath_value}"
)
def set_response_cache(
self,
jsonpath_value: Text,
cache_name
):
"""将响应结果存入缓存"""
_response_data = jsonpath(json.loads(self.response_data), jsonpath_value)
if _response_data is not False:
CacheHandler.update_cache(cache_name=cache_name, value=_response_data[0])
# Cache(cache_name).set_caches(_response_data[0])
else:
raise ValueNotFoundError("缓存设置失败,程序中未检测到需要缓存的数据。"
f"请求参数: {self.response_data}"
f"提取的 jsonpath 内容: {jsonpath_value}")
def set_caches_main(self):
"""设置缓存"""
if self.current_request_set_cache is not None:
for i in self.current_request_set_cache:
_jsonpath = i.jsonpath
_cache_name = i.name
if i.type == 'request':
self.set_request_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)
elif i.type == 'response':
self.set_response_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
请求后置处理
"""
import ast
import json
from typing import Dict, Text
from jsonpath import jsonpath
from utils.requests_tool.request_control import RequestControl
from utils.read_files_tools.regular_control import cache_regular, sql_regular, regular
from utils.other_tools.jsonpath_date_replace import jsonpath_replace
from utils.mysql_tool.mysql_control import MysqlDB
from utils.logging_tool.log_control import WARNING
from utils.other_tools.models import ResponseData, TearDown, SendRequest, ParamPrepare
from utils.other_tools.exceptions import JsonpathExtractionFailed, ValueNotFoundError
from utils.cache_process.cache_control import CacheHandler
from utils import config
class TearDownHandler:
""" 处理yaml格式后置请求 """
def __init__(self, res: "ResponseData"):
self._res = res
@classmethod
def jsonpath_replace_data(
cls,
replace_key: Text,
replace_value: Dict) -> Text:
""" 通过jsonpath判断出需要替换数据的位置 """
_change_data = replace_key.split(".")
# jsonpath 数据解析
_new_data = jsonpath_replace(
change_data=_change_data,
key_name='_teardown_case',
data_switch=False
)
if not isinstance(replace_value, str):
_new_data += f" = {replace_value}"
# 最终提取到的数据,转换成 _teardown_case[xxx][xxx]
else:
_new_data += f" = '{replace_value}'"
return _new_data
@classmethod
def get_cache_name(
cls,
replace_key: Text,
resp_case_data: Dict) -> None:
"""
获取缓存名称,并且讲提取到的数据写入缓存
"""
if "$set_cache{" in replace_key and "}" in replace_key:
start_index = replace_key.index("$set_cache{")
end_index = replace_key.index("}", start_index)
old_value = replace_key[start_index:end_index + 2]
cache_name = old_value[11:old_value.index("}")]
CacheHandler.update_cache(cache_name=cache_name, value=resp_case_data)
# Cache(cache_name).set_caches(resp_case_data)
@classmethod
def regular_testcase(cls, teardown_case: Dict) -> Dict:
"""处理测试用例中的动态数据"""
test_case = regular(str(teardown_case))
test_case = ast.literal_eval(cache_regular(str(test_case)))
return test_case
@classmethod
def teardown_http_requests(cls, teardown_case: Dict) -> "ResponseData":
"""
发送后置请求
@param teardown_case: 后置用例
@return:
"""
test_case = cls.regular_testcase(teardown_case)
res = RequestControl(test_case).http_request(
dependent_switch=False
)
return res
def dependent_type_response(
self,
teardown_case_data: "SendRequest",
resp_data: Dict) -> Text:
"""
判断依赖类型为当前执行用例响应内容
:param : teardown_case_data: teardown中的用例内容
:param : resp_data: 需要替换的内容
:return:
"""
_replace_key = teardown_case_data.replace_key
_response_dependent = jsonpath(
obj=resp_data,
expr=teardown_case_data.jsonpath
)
# 如果提取到数据,则进行下一步
if _response_dependent is not False:
_resp_case_data = _response_dependent[0]
data = self.jsonpath_replace_data(
replace_key=_replace_key,
replace_value=_resp_case_data
)
else:
raise JsonpathExtractionFailed(
f"jsonpath提取失败,替换内容: {resp_data} \n"
f"jsonpath: {teardown_case_data.jsonpath}"
)
return data
def dependent_type_request(
self,
teardown_case_data: Dict,
request_data: Dict) -> None:
"""
判断依赖类型为请求内容
:param : teardown_case_data: teardown中的用例内容
:param : request_data: 需要替换的内容
:return:
"""
try:
_request_set_value = teardown_case_data['set_value']
_request_dependent = jsonpath(
obj=request_data,
expr=teardown_case_data['jsonpath']
)
if _request_dependent is not False:
_request_case_data = _request_dependent[0]
self.get_cache_name(
replace_key=_request_set_value,
resp_case_data=_request_case_data
)
else:
raise JsonpathExtractionFailed(
f"jsonpath提取失败,替换内容: {request_data} \n"
f"jsonpath: {teardown_case_data['jsonpath']}"
)
except KeyError as exc:
raise ValueNotFoundError("teardown中缺少set_value参数,请检查用例是否正确") from exc
def dependent_self_response(
self,
teardown_case_data: "ParamPrepare",
res: Dict,
resp_data: Dict) -> None:
"""
判断依赖类型为依赖用例ID自己响应的内容
:param : teardown_case_data: teardown中的用例内容
:param : resp_data: 需要替换的内容
:param : res: 接口响应的内容
:return:
"""
try:
_set_value = teardown_case_data.set_cache
_response_dependent = jsonpath(
obj=res,
expr=teardown_case_data.jsonpath
)
# 如果提取到数据,则进行下一步
if _response_dependent is not False:
_resp_case_data = _response_dependent[0]
# 拿到 set_cache 然后将数据写入缓存
# Cache(_set_value).set_caches(_resp_case_data)
CacheHandler.update_cache(cache_name=_set_value, value=_resp_case_data)
self.get_cache_name(
replace_key=_set_value,
resp_case_data=_resp_case_data
)
else:
raise JsonpathExtractionFailed(
f"jsonpath提取失败,替换内容: {resp_data} \n"
f"jsonpath: {teardown_case_data.jsonpath}")
except KeyError as exc:
raise ValueNotFoundError("teardown中缺少set_cache参数,请检查用例是否正确") from exc
@classmethod
def dependent_type_cache(cls, teardown_case: "SendRequest") -> Text:
"""
判断依赖类型为从缓存中处理
:param : teardown_case_data: teardown中的用例内容
:return:
"""
if teardown_case.dependent_type == 'cache':
_cache_name = teardown_case.cache_data
_replace_key = teardown_case.replace_key
# 通过jsonpath判断出需要替换数据的位置
_change_data = _replace_key.split(".")
_new_data = jsonpath_replace(
change_data=_change_data,
key_name='_teardown_case',
data_switch=False
)
# jsonpath 数据解析
value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
if any(i in _cache_name for i in value_types) is True:
# _cache_data = Cache(_cache_name.split(':')[1]).get_cache()
_cache_data = CacheHandler.get_cache(_cache_name.split(':')[1])
_new_data += f" = {_cache_data}"
# 最终提取到的数据,转换成 _teardown_case[xxx][xxx]
else:
# _cache_data = Cache(_cache_name).get_cache()
_cache_data = CacheHandler.get_cache(_cache_name)
_new_data += f" = '{_cache_data}'"
return _new_data
def send_request_handler(
self, data: "TearDown",
resp_data: Dict,
request_data: Dict
) -> None:
"""
后置请求处理
@return:
"""
_send_request = data.send_request
_case_id = data.case_id
# _teardown_case = ast.literal_eval(Cache('case_process').get_cache())[_case_id]
_teardown_case = CacheHandler.get_cache(_case_id)
for i in _send_request:
if i.dependent_type == 'cache':
exec(self.dependent_type_cache(teardown_case=i))
# 判断从响应内容提取数据
if i.dependent_type == 'response':
exec(
self.dependent_type_response(
teardown_case_data=i,
resp_data=resp_data)
)
# 判断请求中的数据
elif i.dependent_type == 'request':
self.dependent_type_request(
teardown_case_data=i,
request_data=request_data
)
test_case = self.regular_testcase(_teardown_case)
self.teardown_http_requests(test_case)
def param_prepare_request_handler(
self,
data: "TearDown",
resp_data: Dict) -> None:
"""
前置请求处理
@param data:
@param resp_data:
@return:
"""
_case_id = data.case_id
# _teardown_case = ast.literal_eval(Cache('case_process').get_cache())[_case_id]
_teardown_case = CacheHandler.get_cache(_case_id)
_param_prepare = data.param_prepare
res = self.teardown_http_requests(_teardown_case)
for i in _param_prepare:
# 判断请求类型为自己,拿到当前case_id自己的响应
if i.dependent_type == 'self_response':
self.dependent_self_response(
teardown_case_data=i,
resp_data=resp_data,
res=json.loads(res.response_data)
)
def teardown_handle(self) -> None:
"""
为什么在这里需要单独区分 param_prepare 和 send_request
假设此时我们有用例A,teardown中我们需要执行用例B
那么考虑用户可能需要获取获取teardown的用例B的响应内容,也有可能需要获取用例A的响应内容,
因此我们这里需要通过关键词去做区分。这里需要考虑到,假设我们需要拿到B用例的响应,那么就需要先发送请求然后在拿到响应数据
那如果我们需要拿到A接口的响应,此时我们就不需要在额外发送请求了,因此我们需要区分一个是前置准备param_prepare,
一个是发送请求send_request
@return:
"""
# 拿到用例信息
_teardown_data = self._res.teardown
# 获取接口的响应内容
_resp_data = self._res.response_data
# 获取接口的请求参数
_request_data = self._res.yaml_data.data
# 判断如果没有 teardown
if _teardown_data is not None:
# 循环 teardown中的接口
for _data in _teardown_data:
if _data.param_prepare is not None:
self.param_prepare_request_handler(
data=_data,
resp_data=json.loads(_resp_data)
)
elif _data.send_request is not None:
self.send_request_handler(
data=_data,
request_data=_request_data,
resp_data=json.loads(_resp_data)
)
self.teardown_sql()
def teardown_sql(self) -> None:
"""处理后置sql"""
sql_data = self._res.teardown_sql
_response_data = self._res.response_data
if sql_data is not None:
for i in sql_data:
if config.mysql_db.switch:
_sql_data = sql_regular(value=i, res=json.loads(_response_data))
MysqlDB().execute(cache_regular(_sql_data))
else:
WARNING.logger.warning("程序中检查到您数据库开关为关闭状态,已为您跳过删除sql: %s", i)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
import time
from typing import Text
from datetime import datetime
def count_milliseconds():
"""
计算时间
:return:
"""
access_start = datetime.now()
access_end = datetime.now()
access_delta = (access_end - access_start).seconds * 1000
return access_delta
def timestamp_conversion(time_str: Text) -> int:
"""
时间戳转换,将日期格式转换成时间戳
:param time_str: 时间
:return:
"""
try:
datetime_format = datetime.strptime(str(time_str), "%Y-%m-%d %H:%M:%S")
timestamp = int(
time.mktime(datetime_format.timetuple()) * 1000.0
+ datetime_format.microsecond / 1000.0
)
return timestamp
except ValueError as exc:
raise ValueError('日期格式错误, 需要传入得格式为 "%Y-%m-%d %H:%M:%S" ') from exc
def time_conversion(time_num: int):
"""
时间戳转换成日期
:param time_num:
:return:
"""
if isinstance(time_num, int):
time_stamp = float(time_num / 1000)
time_array = time.localtime(time_stamp)
other_style_time = time.strftime("%Y-%m-%d %H:%M:%S", time_array)
return other_style_time
def now_time():
"""
获取当前时间, 日期格式: 2021-12-11 12:39:25
:return:
"""
localtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
return localtime
def now_time_day():
"""
获取当前时间, 日期格式: 2021-12-11
:return:
"""
localtime = time.strftime("%Y-%m-%d", time.localtime())
return localtime
def get_time_for_min(minute: int) -> int:
"""
获取几分钟后的时间戳
@param minute: 分钟
@return: N分钟后的时间戳
"""
return int(time.time() + 60 * minute) * 1000
def get_now_time() -> int:
"""
获取当前时间戳, 整形
@return: 当前时间戳
"""
return int(time.time()) * 1000
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment