├── .coverage
├── .gitignore
├── Files
├── image
│ └── test.png
└── 自动化异常测试用例.xlsx
├── README.md
├── common
├── __init__.py
├── config.yaml
└── setting.py
├── data
├── Collect
│ ├── collect_addtool.yaml
│ ├── collect_delete_tool.yaml
│ ├── collect_tool_list.yaml
│ └── collect_update_tool.yaml
├── Login
│ └── login.yaml
├── UserInfo
│ └── get_user_info.yaml
└── __init__.py
├── logs
└── __init__.py
├── pytest.ini
├── requirements.txt
├── run.py
├── test_case
├── Collect
│ ├── test_collect_addtool.py
│ ├── test_collect_delete_tool.py
│ ├── test_collect_tool_list.py
│ └── test_collect_update_tool.py
├── Login
│ └── test_login.py
├── UserInfo
│ └── test_get_user_info.py
├── __init__.py
└── conftest.py
└── utils
├── __init__.py
├── assertion
├── __init__.py
├── assert_control.py
└── assert_type.py
├── cache_process
├── __init__.py
├── cache_control.py
└── redis_control.py
├── logging_tool
├── __init__.py
├── log_control.py
├── log_decorator.py
└── run_time_decorator.py
├── mysql_tool
├── __init__.py
└── mysql_control.py
├── notify
├── __init__.py
├── ding_talk.py
├── lark.py
├── send_mail.py
└── wechat_send.py
├── other_tools
├── __init__.py
├── address_detection.py
├── allure_data
│ ├── __init__.py
│ ├── allure_report_data.py
│ ├── allure_tools.py
│ ├── error_case_excel.py
│ └── 自动化异常测试用例.xlsx
├── exceptions.py
├── get_local_ip.py
├── install_tool
│ ├── __init__.py
│ ├── install_requirements.py
│ └── version_library_comparisons.txt
├── jsonpath_date_replace.py
├── models.py
└── thread_tool.py
├── read_files_tools
├── __init__.py
├── case_automatic_control.py
├── clean_files.py
├── excel_control.py
├── get_all_files_path.py
├── get_yaml_data_analysis.py
├── regular_control.py
├── swagger_for_yaml.py
├── testcase_template.py
└── yaml_control.py
├── recording
├── __init__.py
└── mitmproxy_control.py
├── requests_tool
├── __init__.py
├── dependent_case.py
├── encryption_algorithm_control.py
├── request_control.py
├── set_current_request_cache.py
└── teardown_control.py
└── times_tool
├── __init__.py
└── time_control.py
/.coverage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/.coverage
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ##ignore this file##
2 | /venv/
3 | /target/
4 | /.idea/
5 | /.settings/
6 | /.vscode/
7 | /bin/
8 | /report/
9 | !index.pyc
10 | *.pyc
11 | .classpath
12 | .project
13 | .settings
14 | .idea
15 | ##filter databfile、sln file##
16 | *.mdb
17 | *.ldb
18 | *.sln
19 | ##class file##
20 | *.com
21 | *.class
22 | *.dll
23 | *.exe
24 | *.q
25 | *.o
26 | *.so
27 | # compression file
28 | *.7z
29 | *.dmg
30 | *.gz
31 | *.iso
32 | *.jar
33 | *.rar
34 | *.tar
35 | *.zip
36 | *.via
37 | *.tmp
38 | *.err
39 | *.log
40 | *.iml
41 | # OS generated files #
42 | .DS_Store
43 | .DS_Store?
44 | ._*
45 | .Spotlight-V100
46 | .Trashes
47 | Icon?
48 | ehthumbs.db
49 | Thumbs.db
50 | .factorypath
51 | /.mvn/
52 | /mvnw.cmd
53 | /mvnw
--------------------------------------------------------------------------------
/Files/image/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/Files/image/test.png
--------------------------------------------------------------------------------
/Files/自动化异常测试用例.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/Files/自动化异常测试用例.xlsx
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # api_test
2 | # ApiAutoTestProject
3 | python+pytest+requests+yaml+allure接口自动化测试框架
4 |
5 | 项目详细介绍请转到我的CSDN博客:https://blog.csdn.net/qq350146607/article/details/128939167
6 |
7 | 如有不懂的地方或者需要沟通交流测试技术的话,请加我微信:微信号:clownish-HEP 或者扫描下方二维码:
8 |
9 | 
10 |
11 |
12 | 我创建了一个自动化测试交流群,群内有诸多测试行业同伴,一起学习一起进步!!
13 |
--------------------------------------------------------------------------------
/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/common/__init__.py
--------------------------------------------------------------------------------
/common/config.yaml:
--------------------------------------------------------------------------------
1 | project_name: xxx项目名称
2 |
3 | env: 测试环境
4 | # 测试人员名称,作用于自动生成代码的作者,以及发送企业微信、钉钉通知的测试负责人
5 | tester_name: 高级拳师
6 |
7 | # 域名1
8 | host: https://www.wanandroid.com
9 | # 域名2,支持多个域名配置
10 | app_host:
11 |
12 | # 实时更新用例内容,False时,已生成的代码不会在做变更
13 | # 设置为True的时候,修改yaml文件的用例,代码中的内容会实时更新
14 | real_time_update_test_cases: False
15 |
16 | # 报告通知类型:0: 不发送通知 1:钉钉 2:企业微信通知 3、邮箱通知 4、飞书通知
17 | notification_type: 0
18 | # 收集失败的用例开关,整理成excel报告的形式,自动发送,目前只支持返送企业微信通知
19 | excel_report: False
20 |
21 | # 注意点:
22 | # 之前为了小伙伴们拉下代码执行的时候不受影响,企业微信、钉钉、邮箱的通知配置的都是我的
23 | # 我发现很多拉代码的小伙伴这里配置都没改,所有的通知都发到我这里来了哦~~麻烦看到这里的小伙伴自己改一下相关配置
24 |
25 | # 钉钉相关配置
26 | ding_talk:
27 | # webhook: https://oapi.dingtalk.com/robot/send?access_token=a59902a7e811f93ffe301d8326b07a2acc8aa2a864e7d61ee9fc076481ced2a6
28 | # secret: SECdea6489dfcc3b9259da943c5ae38d3530696f2fa83ac72a9ee716e9511675b9b
29 | webhook:
30 | secret:
31 | # 数据库相关配置
32 | mysql_db:
33 | # 数据库开关
34 | switch: False
35 | host:
36 | user: root
37 | password: '123456'
38 | port: 3306
39 |
40 | # 镜像源
41 | mirror_source: http://mirrors.aliyun.com/pypi/simple/
42 |
43 | # 企业通知的相关配置
44 | wechat:
45 | webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=22748687-fa3b-4e48-a5d7-0502cef422b4
46 |
47 |
48 | ### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
49 | ### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
50 | ### 邮箱必填,需要全部都配置好,程序运行失败时,会发送邮件通知!!!!
51 | ### 重要的事情说三遍
52 | email:
53 | send_user:
54 | email_host: smtp.qq.com
55 | # 自己到QQ邮箱中配置stamp_key
56 | stamp_key:
57 | # 收件人改成自己的邮箱
58 | send_list:
59 |
60 | # 飞书通知
61 | lark:
62 | webhook:
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/common/setting.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | import os
4 | from typing import Text
5 |
6 |
7 | def root_path():
8 | """ 获取 根路径 """
9 | path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | return path
11 |
12 |
13 | def ensure_path_sep(path: Text) -> Text:
14 | """兼容 windows 和 linux 不同环境的操作系统路径 """
15 | if "/" in path:
16 | path = os.sep.join(path.split("/"))
17 |
18 | if "\\" in path:
19 | path = os.sep.join(path.split("\\"))
20 |
21 | return root_path() + path
22 |
23 |
--------------------------------------------------------------------------------
/data/Collect/collect_addtool.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 收藏模块
5 | allureStory: 收藏网址接口
6 |
7 | collect_addtool_01:
8 | host: ${{host()}}
9 | url: /lg/collect/addtool/json
10 | method: POST
11 | detail: 新增收藏网址接口
12 | headers:
13 | # 这里cookie的值,写的是存入缓存的名称
14 | cookie: $cache{login_cookie}
15 | # 请求的数据,是 params 还是 json、或者file、data
16 | requestType: data
17 | # 是否执行,空或者 true 都会执行
18 | is_run:
19 | data:
20 | name: 自动化
21 | link: https://gitee.com/yu_xiao_qi/pytest-auto-api2
22 |
23 | dependence_case: False
24 | # 依赖的数据
25 | dependence_case_data:
26 | assert:
27 | # 断言接口状态码
28 | errorCode:
29 | jsonpath: $.errorCode
30 | type: ==
31 | value: 0
32 | AssertType:
33 | message: "errorCode 断言为 0"
34 |
35 | current_request_set_cache:
36 | - type: response
37 | jsonpath: $.data.id
38 | # 自定义的缓存名称
39 | name: yushaoqi_sql
40 | sql:
41 | teardown:
42 | - case_id: collect_delete_tool_01
43 | send_request:
44 | - dependent_type: response
45 | jsonpath: $.data.id
46 | replace_key: $.data.id
47 |
48 | teardown_sql:
49 | - UPDATE `api_test`.`ysq_test` SET `name` = '$json($.data.id)$' WHERE `name` = '2' LIMIT 1
50 |
51 | collect_addtool_02:
52 | host: ${{host()}}
53 | url: /lg/collect/addtool/json
54 | method: POST
55 | detail: 未登录状态下新增收藏网址
56 | headers:
57 | Content-Type: multipart/form-data;
58 | # 这里cookie的值,写的是存入缓存的名称
59 | # 请求的数据,是 params 还是 json、或者file、data
60 | requestType: data
61 | # 是否执行,空或者 true 都会执行
62 | is_run:
63 | data:
64 |
65 | name: 自动生成收藏网址${{random_int()}}
66 | link: https://gitee.com/yu_xiao_qi/pytest-auto-api2
67 | # 是否有依赖业务,为空或者false则表示没有
68 | dependence_case: False
69 | # 依赖的数据
70 | dependence_case_data:
71 | assert:
72 | status_code: 200
73 | # 断言接口状态码
74 | errorCode:
75 | # 断言接口状态码
76 | jsonpath: $.errorCode
77 | type: ==
78 | value: -1001
79 | AssertType:
80 | errorMsg:
81 | jsonpath: $.errorMsg
82 | type: ==
83 | value: '请先登录!'
84 | AssertType:
85 | sql:
86 |
87 |
--------------------------------------------------------------------------------
/data/Collect/collect_delete_tool.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 收藏模块
5 | allureStory: 删除收藏网站接口
6 |
7 | collect_delete_tool_01:
8 | host: ${{host()}}
9 | url: /lg/collect/deletetool/json
10 | method: POST
11 | detail: 正常删除收藏网站
12 | headers:
13 | Content-Type: multipart/form-data;
14 | # 这里cookie的值,写的是存入缓存的名称
15 | cookie: $cache{login_cookie}
16 | # 请求的数据,是 params 还是 json、或者file、data
17 | requestType: data
18 | # 是否执行,空或者 true 都会执行
19 | is_run:
20 | data:
21 | id: $cache{collect_delete_tool_01_id}
22 | id2: 2
23 | dependence_case: True
24 | # 依赖的数据
25 | dependence_case_data:
26 | - case_id: collect_addtool_01
27 | dependent_data:
28 | - dependent_type: response
29 | jsonpath: $.data.id
30 | set_cache: collect_delete_tool_01_id
31 |
32 | assert:
33 | # 断言接口状态码
34 | errorCode:
35 | jsonpath: $.errorCode
36 | type: ==
37 | value: 0
38 | AssertType:
39 | sql:
40 |
41 |
42 | collect_delete_tool_02:
43 | host: ${{host()}}
44 | url: /lg/collect/deletetool/json
45 | method: POST
46 | detail: 正常删除不存在的ID数据(接口未完成此功能,跳过该条用例)
47 | headers:
48 | Content-Type: multipart/form-data;
49 | # 这里cookie的值,写的是存入缓存的名称
50 | cookie: $cache{login_cookie}
51 | # 请求的数据,是 params 还是 json、或者file、data
52 | requestType: data
53 | # 是否执行,空或者 true 都会执行
54 | is_run: False
55 | data:
56 | id: 111
57 | # 是否有依赖业务,为空或者false则表示没有
58 | dependence_case: True
59 | # 依赖的数据
60 | dependence_case_data:
61 | - case_id: collect_addtool_01
62 | dependent_data:
63 | - dependent_type: response
64 | jsonpath: $.data.id
65 | replace_key: $.data.id
66 | assert:
67 | # 断言接口状态码
68 | errorCode:
69 | jsonpath: $.errorCode
70 | type: ==
71 | value: 0
72 | AssertType:
73 | sql:
74 |
--------------------------------------------------------------------------------
/data/Collect/collect_tool_list.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 收藏模块
5 | allureStory: 收藏网址列表接口
6 |
7 | collect_tool_list_01:
8 | host: ${{host()}}
9 | url: /lg/collect/usertools/json
10 | method: GET
11 | detail: 查看收藏网址列表接口
12 | headers:
13 | Content-Type: multipart/form-data;
14 | # 这里cookie的值,写的是存入缓存的名称
15 | cookie: $cache{login_cookie}
16 | # 请求的数据,是 params 还是 json、或者file、data
17 | requestType: None
18 | # 是否执行,空或者 true 都会执行
19 | is_run:
20 | data:
21 | # 是否有依赖业务,为空或者false则表示没有
22 | dependence_case: True
23 | # 依赖的数据
24 | dependence_case_data:
25 | - case_id: self
26 | dependent_data:
27 | - dependent_type: sqlData
28 | jsonpath: $.business_type
29 | set_cache: yushaoqi
30 |
31 | assert:
32 | # 断言接口状态码
33 | errorCode:
34 | jsonpath: $.errorCode
35 | type: ==
36 | value: 0
37 | AssertType:
38 | status_code: 200
39 | sql:
40 | setup_sql:
41 | - SELECT * FROM `api_test`.`t_open_field_cfg_copy1` LIMIT 0,1;
42 | sleep: 2
43 |
44 |
--------------------------------------------------------------------------------
/data/Collect/collect_update_tool.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 收藏模块
5 | allureStory: 编辑收藏网址接口
6 |
7 | collect_update_tool_01:
8 | host: ${{host()}}
9 | url: /lg/collect/addtool/json
10 | method: POST
11 | detail: 编辑收藏网址
12 | headers:
13 | Content-Type: multipart/form-data;
14 | # 这里cookie的值,写的是存入缓存的名称
15 | cookie: $cache{login_cookie}
16 | # 请求的数据,是 params 还是 json、或者file、data
17 | requestType: data
18 | # 是否执行,空或者 true 都会执行
19 | is_run: False
20 | data:
21 | name: 自动化编辑网址名称
22 | link: https://gitee.com/yu_xiao_qi/pytest-auto-api2
23 | id:
24 | # 是否有依赖业务,为空或者false则表示没有
25 | dependence_case: True
26 | # 依赖的数据
27 | dependence_case_data:
28 | - case_id: collect_addtool_01
29 | dependent_data:
30 | - dependent_type: response
31 | jsonpath: $.data.id
32 | replace_key: $.data.id
33 | assert:
34 | # 断言接口状态码
35 | errorCode:
36 | jsonpath: $.errorCode
37 | type: ==
38 | value: 0
39 | AssertType:
40 | sql:
41 | teardown:
42 |
43 | # 先搜索
44 | - case_id: collect_tool_list_01
45 | param_prepare:
46 | - dependent_type: self_response
47 | jsonpath: $.data[-1:].id
48 | set_cache: $set_cache{artile_id}
49 |
50 | # 删除
51 | - case_id: collect_delete_tool_01
52 | send_request:
53 | # 删除从缓存中拿数据
54 | - dependent_type: cache
55 | cache_data: int:artile_id
56 | replace_key: $.data.id
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/data/Login/login.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 登录模块
5 | allureStory: 登录
6 |
7 | login_01:
8 | host: ${{host()}}
9 | url: /user/login
10 | method: POST
11 | detail: 正常登录
12 | headers:
13 | # Content-Type: multipart/form-data;
14 | # 请求的数据,是 params 还是 json、或者file、data
15 | requestType: data
16 | # 是否执行,空或者 true 都会执行
17 | is_run:
18 | data:
19 | username: '18800000001'
20 | password: '123456'
21 | # 是否有依赖业务,为空或者false则表示没有
22 | dependence_case: False
23 | # 依赖的数据
24 | dependence_case_data:
25 | assert:
26 | # 断言接口状态码
27 | errorCode:
28 | jsonpath: $.errorCode
29 | type: ==
30 | value: 0
31 | AssertType:
32 | # 断言接口返回的username
33 | username:
34 | jsonpath: $.data.username
35 | type: ==
36 | value: '18800000001'
37 | AssertType:
38 | sql:
39 |
40 | login_02:
41 | host: ${{host()}}
42 | url: /user/login
43 | method: POST
44 | detail: 输入错误的密码
45 | headers:
46 | Content-Type: multipart/form-data;
47 | # 请求的数据,是 params 还是 json、或者file、data
48 | requestType: data
49 | # 是否执行,空或者 true 都会执行
50 | is_run:
51 | data:
52 | username: '18800000001'
53 | password: '12345'
54 | # 是否有依赖业务,为空或者false则表示没有
55 | dependence_case: False
56 | # 依赖的数据
57 | dependence_case_data:
58 | assert:
59 | # 断言接口状态码
60 | errorCode:
61 | jsonpath: $.errorCode
62 | type: ==
63 | value: -1
64 | AssertType:
65 | # 断言接口返回的username
66 | errorMsg:
67 | jsonpath: $.errorMsg
68 | type: ==
69 | value: "账号密码不匹配!"
70 | AssertType:
71 | sql:
72 |
73 | login_03:
74 | host: ${{host()}}
75 | url: /user/login
76 | method: POST
77 | detail: 登录密码为空
78 | headers:
79 | Content-Type: multipart/form-data;
80 | # 请求的数据,是 params 还是 json、或者file、data
81 | requestType: data
82 | # 是否执行,空或者 true 都会执行
83 | is_run:
84 | data:
85 | username: '18800000001'
86 | password:
87 | # 是否有依赖业务,为空或者false则表示没有
88 | dependence_case: False
89 | # 依赖的数据
90 | dependence_case_data:
91 | assert:
92 | # 断言接口状态码
93 | errorCode:
94 | jsonpath: $.errorCode
95 | type: ==
96 | value: -1
97 | AssertType:
98 | # 断言接口返回的username
99 | errorMsg:
100 | jsonpath: $.errorMsg
101 | type: ==
102 | value: "账号密码不匹配!"
103 | AssertType:
104 | sql:
105 |
106 | login_04:
107 | host: ${{host()}}
108 | url: /user/login
109 | method: POST
110 | detail: 输入非1开头的手机号码
111 | headers:
112 | Content-Type: multipart/form-data;
113 | # 请求的数据,是 params 还是 json、或者file、data
114 | requestType: data
115 | # 是否执行,空或者 true 都会执行
116 | is_run:
117 | data:
118 | username: '28800000001'
119 | password: '12345'
120 | # 是否有依赖业务,为空或者false则表示没有
121 | dependence_case: False
122 | # 依赖的数据
123 | dependence_case_data:
124 | assert:
125 | # 断言接口状态码
126 | errorCode:
127 | jsonpath: $.errorCode
128 | type: ==
129 | value: -1
130 | AssertType:
131 | # 断言接口返回的username
132 | errorMsg:
133 | jsonpath: $.errorMsg
134 | type: ==
135 | value: "账号密码不匹配!"
136 | AssertType:
137 | sql:
138 |
139 | login_05:
140 | host: ${{host()}}
141 | url: /user/login
142 | method: POST
143 | detail: 输入手机号码小于11位
144 | headers:
145 | Content-Type: multipart/form-data;
146 | # 请求的数据,是 params 还是 json、或者file、data
147 | requestType: data
148 | # 是否执行,空或者 true 都会执行
149 | is_run:
150 | data:
151 | username: '1880000000'
152 | password: '12345'
153 | # 是否有依赖业务,为空或者false则表示没有
154 | dependence_case: False
155 | # 依赖的数据
156 | dependence_case_data:
157 | assert:
158 | # 断言接口状态码
159 | errorCode:
160 | jsonpath: $.errorCode
161 | type: ==
162 | value: -1
163 | AssertType:
164 | # 断言接口返回的username
165 | errorMsg:
166 | jsonpath: $.errorMsg
167 | type: ==
168 | value: "账号密码不匹配!"
169 | AssertType:
170 | sql:
171 |
172 | login_06:
173 | host: ${{host()}}
174 | url: /user/login
175 | method: POST
176 | detail: 输入手机号码大于于11位
177 | headers:
178 | Content-Type: multipart/form-data;
179 | # 请求的数据,是 params 还是 json、或者file、data
180 | requestType: data
181 | # 是否执行,空或者 true 都会执行
182 | is_run:
183 | data:
184 | username: '18800000000'
185 | password: '12345'
186 | # 是否有依赖业务,为空或者false则表示没有
187 | dependence_case: False
188 | # 依赖的数据
189 | dependence_case_data:
190 | assert:
191 | # 断言接口状态码
192 | errorCode:
193 | jsonpath: $.errorCode
194 | type: ==
195 | value: -1
196 | AssertType:
197 | # 断言接口返回的username
198 | errorMsg:
199 | jsonpath: $.errorMsg
200 | type: ==
201 | value: "账号密码不匹配!"
202 | AssertType:
203 | sql:
204 |
205 | login_07:
206 | host: ${{host()}}
207 | url: /user/login
208 | method: POST
209 | detail: 手机号码为空
210 | headers:
211 | Content-Type: multipart/form-data;
212 | # 请求的数据,是 params 还是 json、或者file、data
213 | requestType: data
214 | # 是否执行,空或者 true 都会执行
215 | is_run:
216 | data:
217 | username:
218 | password: '12345'
219 | # 是否有依赖业务,为空或者false则表示没有
220 | dependence_case: False
221 | # 依赖的数据
222 | dependence_case_data:
223 | assert:
224 | # 断言接口状态码
225 | errorCode:
226 | jsonpath: $.errorCode
227 | type: ==
228 | value: -1
229 | AssertType:
230 | # 断言接口返回的username
231 | errorMsg:
232 | jsonpath: $.errorMsg
233 | type: ==
234 | value: "账号密码不匹配!"
235 | AssertType:
236 | sql:
237 |
238 | login_08:
239 | host: ${{host()}}
240 | url: /user/login
241 | method: POST
242 | detail: 手机号码首位包含空格
243 | headers:
244 | Content-Type: multipart/form-data;
245 | # 请求的数据,是 params 还是 json、或者file、data
246 | requestType: data
247 | # 是否执行,空或者 true 都会执行
248 | is_run:
249 | data:
250 | username: ' 18867507063 '
251 | password: '12345'
252 | # 是否有依赖业务,为空或者false则表示没有
253 | dependence_case: False
254 | # 依赖的数据
255 | dependence_case_data:
256 | assert:
257 | # 断言接口状态码
258 | errorCode:
259 | jsonpath: $.errorCode
260 | type: ==
261 | value: -1
262 | AssertType:
263 | # 断言接口返回的username
264 | errorMsg:
265 | jsonpath: $.errorMsg
266 | type: ==
267 | value: "账号密码不匹配!"
268 | AssertType:
269 | sql:
270 |
--------------------------------------------------------------------------------
/data/UserInfo/get_user_info.yaml:
--------------------------------------------------------------------------------
1 | # 公共参数
2 | case_common:
3 | allureEpic: 开发平台接口
4 | allureFeature: 个人信息模块
5 | allureStory: 个人信息接口
6 |
7 | get_user_info_01:
8 | host: ${{host()}}
9 | url: /user/lg/userinfo/json
10 | method: GET
11 | detail: 正常获取个人身份信息
12 | headers:
13 | Content-Type: multipart/form-data;
14 | # 这里cookie的值,写的是存入缓存的名称
15 | cookie: $cache{login_cookie}
16 | # 请求的数据,是 params 还是 json、或者file、data
17 | requestType: data
18 | # 是否执行,空或者 true 都会执行
19 | is_run:
20 | data:
21 | # 是否有依赖业务,为空或者false则表示没有
22 | dependence_case: False
23 | # 依赖的数据
24 | dependence_case_data:
25 | assert:
26 | # 断言接口状态码
27 | errorCode:
28 | jsonpath: $.errorCode
29 | type: ==
30 | value: 0
31 | AssertType:
32 | # 断言接口返回的username
33 | username:
34 | jsonpath: $.data.userInfo.username
35 | type: ==
36 | value: '18800000001'
37 | AssertType:
38 | sql:
39 |
--------------------------------------------------------------------------------
/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/data/__init__.py
--------------------------------------------------------------------------------
/logs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/logs/__init__.py
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -p no:warnings
3 | testpaths = test_case/
4 | python_files = test_*.py
5 | python_classes = Test*
6 | python_function = test_*
7 |
8 | markers =
9 | smoke: 冒烟测试
10 |
11 |
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==0.8.0
2 | allure-pytest==2.9.45
3 | allure-python-commons==2.9.45
4 | asgiref==3.5.1
5 | atomicwrites==1.4.0
6 | attrs==21.2.0
7 | blinker==1.4
8 | Brotli==1.0.9
9 | certifi==2021.10.8
10 | cffi==1.15.0
11 | chardet==4.0.0
12 | charset-normalizer==2.0.7
13 | click==8.1.3
14 | colorama==0.4.4
15 | colorlog==6.6.0
16 | cryptography==36.0.0
17 | DingtalkChatbot==1.5.3
18 | et-xmlfile==1.1.0
19 | execnet==1.9.0
20 | Faker==9.8.3
21 | Flask==2.0.3
22 | h11==0.13.0
23 | h2==4.1.0
24 | hpack==4.0.0
25 | httptools==0.4.0
26 | hyperframe==6.0.1
27 | idna==3.3
28 | iniconfig==1.1.0
29 | itchat==1.3.10
30 | itsdangerous==2.1.2
31 | Jinja2==3.1.2
32 | jsonpath==0.82
33 | kaitaistruct==0.9
34 | ldap3==2.9.1
35 | MarkupSafe==2.1.1
36 | mitmproxy~=8.1.0
37 | msgpack==1.0.3
38 | multidict==6.0.2
39 | openpyxl==3.0.9
40 | packaging==21.3
41 | passlib==1.7.4
42 | pluggy==1.0.0
43 | protobuf==3.19.4
44 | publicsuffix2==2.20191221
45 | py==1.11.0
46 | pyasn1==0.4.8
47 | pycparser==2.21
48 | pydivert==2.1.0
49 | PyMySQL==1.0.2
50 | pyOpenSSL==21.0.0
51 | pyparsing==3.0.6
52 | pyperclip==1.8.2
53 | pypng==0.0.21
54 | PyQRCode==1.2.1
55 | pytest~=7.1.2
56 | pytest-forked==1.3.0
57 | pytest-xdist==2.4.0
58 | python-dateutil==2.8.2
59 | pywin32==304
60 | PyYAML~=5.4.1
61 | requests==2.26.0
62 | requests-toolbelt==0.9.1
63 | ruamel.yaml==0.17.21
64 | ruamel.yaml.clib==0.2.6
65 | sanic==22.3.1
66 | sanic-routing==22.3.0
67 | six==1.16.0
68 | sortedcontainers==2.4.0
69 | text-unidecode==1.3
70 | toml==0.10.2
71 | tornado==6.1
72 | urllib3==1.26.7
73 | urwid==2.1.2
74 | websockets==10.3
75 | Werkzeug==2.1.2
76 | wsproto==1.1.0
77 | xlrd==2.0.1
78 | xlutils==2.0.0
79 | xlwings==0.27.7
80 | xlwt==1.3.0
81 | zstandard==0.17.0
82 |
83 | pyDes~=2.0.1
84 | crypto~=1.4.1
85 | redis~=4.3.4
86 |
87 | pydantic~=1.8.2
88 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import os
4 | import sys
5 | import traceback
6 | import pytest
7 | from utils.other_tools.models import NotificationType
8 | from utils.other_tools.allure_data.allure_report_data import AllureFileClean
9 | from utils.logging_tool.log_control import INFO
10 | from utils.notify.wechat_send import WeChatSend
11 | from utils.notify.ding_talk import DingTalkSendMsg
12 | from utils.notify.send_mail import SendEmail
13 | from utils.notify.lark import FeiShuTalkChatBot
14 | from utils.other_tools.allure_data.error_case_excel import ErrorCaseExcel
15 | from utils import config
16 |
17 |
18 | def run():
19 | # 从配置文件中获取项目名称
20 | try:
21 | INFO.logger.info(
22 | """
23 | _ _ _ _____ _
24 | __ _ _ __ (_) / \\ _ _| |_ __|_ _|__ ___| |_
25 | / _` | '_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __|
26 | | (_| | |_) | |/ ___ \\ |_| | || (_) | | __/\\__ \\ |_
27 | \\__,_| .__/|_/_/ \\_\\__,_|\\__\\___/|_|\\___||___/\\__|
28 | |_|
29 | 开始执行{}项目...
30 | """.format(config.project_name)
31 | )
32 |
33 | # 判断现有的测试用例,如果未生成测试代码,则自动生成
34 | # TestCaseAutomaticGeneration().get_case_automatic()
35 |
36 | pytest.main(['-s', '-W', 'ignore:Module already imported:pytest.PytestWarning',
37 | '--alluredir', './report/tmp', "--clean-alluredir"])
38 |
39 | """
40 | --reruns: 失败重跑次数
41 | --count: 重复执行次数
42 | -v: 显示错误位置以及错误的详细信息
43 | -s: 等价于 pytest --capture=no 可以捕获print函数的输出
44 | -q: 简化输出信息
45 | -m: 运行指定标签的测试用例
46 | -x: 一旦错误,则停止运行
47 | --maxfail: 设置最大失败次数,当超出这个阈值时,则不会在执行测试用例
48 | "--reruns=3", "--reruns-delay=2"
49 | """
50 |
51 | os.system(r"allure generate ./report/tmp -o ./report/html --clean")
52 |
53 | allure_data = AllureFileClean().get_case_count()
54 | notification_mapping = {
55 | NotificationType.DING_TALK.value: DingTalkSendMsg(allure_data).send_ding_notification,
56 | NotificationType.WECHAT.value: WeChatSend(allure_data).send_wechat_notification,
57 | NotificationType.EMAIL.value: SendEmail(allure_data).send_main,
58 | NotificationType.FEI_SHU.value: FeiShuTalkChatBot(allure_data).post
59 | }
60 |
61 | if config.notification_type != NotificationType.DEFAULT.value:
62 | notification_mapping.get(config.notification_type)()
63 |
64 | if config.excel_report:
65 | ErrorCaseExcel().write_case()
66 |
67 | # 程序运行之后,自动启动报告,如果不想启动报告,可注释这段代码
68 | os.system(f"allure serve ./report/tmp -h 127.0.0.1 -p 9999")
69 |
70 | except Exception:
71 | # 如有异常,相关异常发送邮件
72 | e = traceback.format_exc()
73 | send_email = SendEmail(AllureFileClean.get_case_count())
74 | send_email.error_mail(e)
75 | raise
76 |
77 |
78 | if __name__ == '__main__':
79 | run()
80 |
--------------------------------------------------------------------------------
/test_case/Collect/test_collect_addtool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['collect_addtool_01', 'collect_addtool_02']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("收藏模块")
22 | class TestCollectAddtool:
23 |
24 | @allure.story("收藏网址接口")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_collect_addtool(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_collect_addtool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/Collect/test_collect_delete_tool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['collect_delete_tool_01', 'collect_delete_tool_02']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("收藏模块")
22 | class TestCollectDeleteTool:
23 |
24 | @allure.story("删除收藏网站接口")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_collect_delete_tool(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_collect_delete_tool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/Collect/test_collect_tool_list.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['collect_tool_list_01']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("收藏模块")
22 | class TestCollectToolList:
23 |
24 | @allure.story("收藏网址列表接口")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_collect_tool_list(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_collect_tool_list.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/Collect/test_collect_update_tool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['collect_update_tool_01']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("收藏模块")
22 | class TestCollectUpdateTool:
23 |
24 | @allure.story("编辑收藏网址接口")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_collect_update_tool(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_collect_update_tool.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/Login/test_login.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['login_01', 'login_02', 'login_03', 'login_04', 'login_05', 'login_06', 'login_07', 'login_08']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("登录模块")
22 | class TestLogin:
23 |
24 | @allure.story("登录")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_login(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_login.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/UserInfo/test_get_user_info.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # @Time : 2022-08-17 10:12:54
4 |
5 |
6 | import allure
7 | import pytest
8 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
9 | from utils.assertion.assert_control import Assert
10 | from utils.requests_tool.request_control import RequestControl
11 | from utils.read_files_tools.regular_control import regular
12 | from utils.requests_tool.teardown_control import TearDownHandler
13 |
14 |
15 | case_id = ['get_user_info_01']
16 | TestData = GetTestCase.case_data(case_id)
17 | re_data = regular(str(TestData))
18 |
19 |
20 | @allure.epic("开发平台接口")
21 | @allure.feature("个人信息模块")
22 | class TestGetUserInfo:
23 |
24 | @allure.story("个人信息接口")
25 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
26 | def test_get_user_info(self, in_data, case_skip):
27 | """
28 | :param :
29 | :return:
30 | """
31 | res = RequestControl(in_data).http_request()
32 | TearDownHandler(res).teardown_handle()
33 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
34 | sql_data=res.sql_data, status_code=res.status_code)
35 |
36 |
37 | if __name__ == '__main__':
38 | pytest.main(['test_get_user_info.py', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
39 |
--------------------------------------------------------------------------------
/test_case/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from common.setting import ensure_path_sep
3 | from utils.read_files_tools.get_yaml_data_analysis import CaseData
4 | from utils.read_files_tools.get_all_files_path import get_all_files
5 | from utils.cache_process.cache_control import CacheHandler, _cache_config
6 |
7 |
8 | def write_case_process():
9 | """
10 | 获取所有用例,写入用例池中
11 | :return:
12 | """
13 |
14 | # 循环拿到所有存放用例的文件路径
15 | for i in get_all_files(file_path=ensure_path_sep("\\data"), yaml_data_switch=True):
16 | # 循环读取文件中的数据
17 | case_process = CaseData(i).case_process(case_id_switch=True)
18 | if case_process is not None:
19 | # 转换数据类型
20 | for case in case_process:
21 | for k, v in case.items():
22 | # 判断 case_id 是否已存在
23 | case_id_exit = k in _cache_config.keys()
24 | # 如果case_id 不存在,则将用例写入缓存池中
25 | if case_id_exit is False:
26 | CacheHandler.update_cache(cache_name=k, value=v)
27 | # case_data[k] = v
28 | # 当 case_id 为 True 存在时,则跑出异常
29 | elif case_id_exit is True:
30 | raise ValueError(f"case_id: {k} 存在重复项, 请修改case_id\n"
31 | f"文件路径: {i}")
32 |
33 |
34 | write_case_process()
35 |
--------------------------------------------------------------------------------
/test_case/conftest.py:
--------------------------------------------------------------------------------
1 |
2 | import pytest
3 | import time
4 | import allure
5 | import requests
6 | import ast
7 | from common.setting import ensure_path_sep
8 | from utils.requests_tool.request_control import cache_regular
9 | from utils.logging_tool.log_control import INFO, ERROR, WARNING
10 | from utils.other_tools.models import TestCase
11 | from utils.read_files_tools.clean_files import del_file
12 | from utils.other_tools.allure_data.allure_tools import allure_step, allure_step_no
13 | from utils.cache_process.cache_control import CacheHandler
14 |
15 |
16 | @pytest.fixture(scope="session", autouse=False)
17 | def clear_report():
18 | """如clean命名无法删除报告,这里手动删除"""
19 | del_file(ensure_path_sep("\\report"))
20 |
21 |
22 | @pytest.fixture(scope="session", autouse=True)
23 | def work_login_init():
24 | """
25 | 获取登录的cookie
26 | :return:
27 | """
28 |
29 | url = "https://www.wanandroid.com/user/login"
30 | data = {
31 | "username": 18800000001,
32 | "password": 123456
33 | }
34 | headers = {'Content-Type': 'application/x-www-form-urlencoded'}
35 | # 请求登录接口
36 |
37 | res = requests.post(url=url, data=data, verify=True, headers=headers)
38 | response_cookie = res.cookies
39 |
40 | cookies = ''
41 | for k, v in response_cookie.items():
42 | _cookie = k + "=" + v + ";"
43 | # 拿到登录的cookie内容,cookie拿到的是字典类型,转换成对应的格式
44 | cookies += _cookie
45 | # 将登录接口中的cookie写入缓存中,其中login_cookie是缓存名称
46 | CacheHandler.update_cache(cache_name='login_cookie', value=cookies)
47 |
48 |
49 | def pytest_collection_modifyitems(items):
50 | """
51 | 测试用例收集完成时,将收集到的 item 的 name 和 node_id 的中文显示在控制台上
52 | :return:
53 | """
54 | for item in items:
55 | item.name = item.name.encode("utf-8").decode("unicode_escape")
56 | item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
57 |
58 | # 期望用例顺序
59 | # print("收集到的测试用例:%s" % items)
60 | appoint_items = ["test_get_user_info", "test_collect_addtool", "test_Cart_List", "test_ADD", "test_Guest_ADD",
61 | "test_Clear_Cart_Item"]
62 |
63 | # 指定运行顺序
64 | run_items = []
65 | for i in appoint_items:
66 | for item in items:
67 | module_item = item.name.split("[")[0]
68 | if i == module_item:
69 | run_items.append(item)
70 |
71 | for i in run_items:
72 | run_index = run_items.index(i)
73 | items_index = items.index(i)
74 |
75 | if run_index != items_index:
76 | n_data = items[run_index]
77 | run_index = items.index(n_data)
78 | items[items_index], items[run_index] = items[run_index], items[items_index]
79 |
80 |
81 | def pytest_configure(config):
82 | config.addinivalue_line("markers", 'smoke')
83 | config.addinivalue_line("markers", '回归测试')
84 |
85 |
86 | @pytest.fixture(scope="function", autouse=True)
87 | def case_skip(in_data):
88 | """处理跳过用例"""
89 | in_data = TestCase(**in_data)
90 | if ast.literal_eval(cache_regular(str(in_data.is_run))) is False:
91 | allure.dynamic.title(in_data.detail)
92 | allure_step_no(f"请求URL: {in_data.is_run}")
93 | allure_step_no(f"请求方式: {in_data.method}")
94 | allure_step("请求头: ", in_data.headers)
95 | allure_step("请求数据: ", in_data.data)
96 | allure_step("依赖数据: ", in_data.dependence_case_data)
97 | allure_step("预期数据: ", in_data.assert_data)
98 | pytest.skip()
99 |
100 |
101 | def pytest_terminal_summary(terminalreporter):
102 | """
103 | 收集测试结果
104 | """
105 |
106 | _PASSED = len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown'])
107 | _ERROR = len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown'])
108 | _FAILED = len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown'])
109 | _SKIPPED = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown'])
110 | _TOTAL = terminalreporter._numcollected
111 | _TIMES = time.time() - terminalreporter._sessionstarttime
112 | INFO.logger.error(f"用例总数: {_TOTAL}")
113 | INFO.logger.error(f"异常用例数: {_ERROR}")
114 | ERROR.logger.error(f"失败用例数: {_FAILED}")
115 | WARNING.logger.warning(f"跳过用例数: {_SKIPPED}")
116 | INFO.logger.info("用例执行时长: %.2f" % _TIMES + " s")
117 |
118 | try:
119 | _RATE = _PASSED / _TOTAL * 100
120 | INFO.logger.info("用例成功率: %.2f" % _RATE + " %")
121 | except ZeroDivisionError:
122 | INFO.logger.info("用例成功率: 0.00 %")
123 |
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from utils.read_files_tools.yaml_control import GetYamlData
3 | from common.setting import ensure_path_sep
4 | from utils.other_tools.models import Config
5 |
6 |
7 | _data = GetYamlData(ensure_path_sep("\\common\\config.yaml")).get_yaml_data()
8 | config = Config(**_data)
9 |
10 |
--------------------------------------------------------------------------------
/utils/assertion/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/assertion/__init__.py
--------------------------------------------------------------------------------
/utils/assertion/assert_control.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | 断言类型封装,支持json响应断言、数据库断言
4 | """
5 | import ast
6 | import json
7 | from typing import Text, Dict, Any, Union
8 | from jsonpath import jsonpath
9 | from utils.other_tools.models import AssertMethod
10 | from utils.logging_tool.log_control import ERROR, WARNING
11 | from utils.read_files_tools.regular_control import cache_regular
12 | from utils.other_tools.models import load_module_functions
13 | from utils.assertion import assert_type
14 | from utils.other_tools.exceptions import JsonpathExtractionFailed, SqlNotFound, AssertTypeError
15 | from utils import config
16 |
17 |
18 | class Assert:
19 | """ assert 模块封装 """
20 |
21 | def __init__(self, assert_data: Dict):
22 | self.assert_data = ast.literal_eval(cache_regular(str(assert_data)))
23 | self.functions_mapping = load_module_functions(assert_type)
24 |
25 | @staticmethod
26 | def _check_params(
27 | response_data: Text,
28 | sql_data: Union[Dict, None]) -> bool:
29 | """
30 |
31 | :param response_data: 响应数据
32 | :param sql_data: 数据库数据
33 | :return:
34 | """
35 | if (response_data and sql_data) is not False:
36 | if not isinstance(sql_data, dict):
37 | raise ValueError(
38 | "断言失败,response_data、sql_data的数据类型必须要是字典类型,"
39 | "请检查接口对应的数据是否正确\n"
40 | f"sql_data: {sql_data}, 数据类型: {type(sql_data)}\n"
41 | )
42 | return True
43 |
44 | @staticmethod
45 | def res_sql_data_bytes(res_sql_data: Any) -> Text:
46 | """ 处理 mysql查询出来的数据类型如果是bytes类型,转换成str类型 """
47 | if isinstance(res_sql_data, bytes):
48 | res_sql_data = res_sql_data.decode('utf=8')
49 | return res_sql_data
50 |
51 | def sql_switch_handle(
52 | self,
53 | sql_data: Dict,
54 | assert_value: Any,
55 | key: Text,
56 | values: Any,
57 | resp_data: Dict,
58 | message: Text) -> None:
59 | """
60 |
61 | :param sql_data: 测试用例中的sql
62 | :param assert_value: 断言内容
63 | :param key:
64 | :param values:
65 | :param resp_data: 预期结果
66 | :param message: 预期结果
67 | :return:
68 | """
69 | # 判断数据库为开关为关闭状态
70 | if config.mysql_db.switch is False:
71 | WARNING.logger.warning(
72 | "检测到数据库状态为关闭状态,程序已为您跳过此断言,断言值:%s", values
73 | )
74 | # 数据库开关为开启
75 | if config.mysql_db.switch:
76 | # 走正常SQL断言逻辑
77 | if sql_data != {'sql': None}:
78 | res_sql_data = jsonpath(sql_data, assert_value)
79 | if res_sql_data is False:
80 | raise JsonpathExtractionFailed(
81 | f"数据库断言内容jsonpath提取失败, 当前jsonpath内容: {assert_value}\n"
82 | f"数据库返回内容: {sql_data}"
83 | )
84 |
85 | # 判断mysql查询出来的数据类型如果是bytes类型,转换成str类型
86 | res_sql_data = self.res_sql_data_bytes(res_sql_data[0])
87 | name = AssertMethod(self.assert_data[key]['type']).name
88 | self.functions_mapping[name](resp_data[0], res_sql_data, str(message))
89 |
90 | # 判断当用例走的数据数据库断言,但是用例中未填写SQL
91 | else:
92 | raise SqlNotFound("请在用例中添加您要查询的SQL语句。")
93 |
94 | def assert_type_handle(
95 | self,
96 | assert_types: Union[Text, None],
97 | sql_data: Union[Dict, None],
98 | assert_value: Any,
99 | key: Text,
100 | values: Dict,
101 | resp_data: Any,
102 | message: Text
103 | ) -> None:
104 | """处理断言类型"""
105 | # 判断断言类型
106 | if assert_types == 'SQL':
107 | self.sql_switch_handle(
108 | sql_data=sql_data,
109 | assert_value=assert_value,
110 | key=key,
111 | values=values,
112 | resp_data=resp_data,
113 | message=message
114 | )
115 |
116 | # 判断assertType为空的情况下,则走响应断言
117 | elif assert_types is None:
118 | name = AssertMethod(self.assert_data[key]['type']).name
119 | self.functions_mapping[name](resp_data[0], assert_value, message)
120 | else:
121 | raise AssertTypeError("断言失败,目前只支持数据库断言和响应断言")
122 |
123 | @classmethod
124 | def _message(cls, value):
125 | _message = ""
126 | if jsonpath(obj=value, expr="$.message") is not False:
127 | _message = value['message']
128 | return _message
129 |
130 | def assert_equality(
131 | self,
132 | response_data: Text,
133 | sql_data: Dict,
134 | status_code: int) -> None:
135 | """ assert 断言处理 """
136 | # 判断数据类型
137 | if self._check_params(response_data, sql_data) is not False:
138 | for key, values in self.assert_data.items():
139 | if key == "status_code":
140 | assert status_code == values
141 | else:
142 | assert_value = self.assert_data[key]['value'] # 获取 yaml 文件中的期望value值
143 | assert_jsonpath = self.assert_data[key]['jsonpath'] # 获取到 yaml断言中的jsonpath的数据
144 | assert_types = self.assert_data[key]['AssertType']
145 | # 从yaml获取jsonpath,拿到对象的接口响应数据
146 | resp_data = jsonpath(json.loads(response_data), assert_jsonpath)
147 | message = self._message(value=values)
148 | # jsonpath 如果数据获取失败,会返回False,判断获取成功才会执行如下代码
149 | if resp_data is not False:
150 | # 判断断言类型
151 | self.assert_type_handle(
152 | assert_types=assert_types,
153 | sql_data=sql_data,
154 | assert_value=assert_value,
155 | key=key,
156 | values=values,
157 | resp_data=resp_data,
158 | message=message
159 | )
160 | else:
161 | ERROR.logger.error("JsonPath值获取失败 %s ", assert_jsonpath)
162 | raise JsonpathExtractionFailed(f"JsonPath值获取失败 {assert_jsonpath}")
163 |
164 |
165 | if __name__ == '__main__':
166 | pass
167 |
--------------------------------------------------------------------------------
/utils/assertion/assert_type.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | """
4 | Assert 断言类型
5 | """
6 |
7 | from typing import Any, Union, Text
8 |
9 |
10 | def equals(
11 | check_value: Any, expect_value: Any, message: Text = ""
12 | ):
13 | """判断是否相等"""
14 |
15 | assert check_value == expect_value, message
16 |
17 |
18 | def less_than(
19 | check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
20 | ):
21 | """判断实际结果小于预期结果"""
22 | assert check_value < expect_value, message
23 |
24 |
25 | def less_than_or_equals(
26 | check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""):
27 |
28 | """判断实际结果小于等于预期结果"""
29 | assert check_value <= expect_value, message
30 |
31 |
32 | def greater_than(
33 | check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
34 | ):
35 | """判断实际结果大于预期结果"""
36 | assert check_value > expect_value, message
37 |
38 |
39 | def greater_than_or_equals(
40 | check_value: Union[int, float], expect_value: Union[int, float], message: Text = ""
41 | ):
42 | """判断实际结果大于等于预期结果"""
43 | assert check_value >= expect_value, message
44 |
45 |
46 | def not_equals(
47 | check_value: Any, expect_value: Any, message: Text = ""
48 | ):
49 | """判断实际结果不等于预期结果"""
50 | assert check_value != expect_value, message
51 |
52 |
53 | def string_equals(
54 | check_value: Text, expect_value: Any, message: Text = ""
55 | ):
56 | """判断字符串是否相等"""
57 | assert check_value == expect_value, message
58 |
59 |
60 | def length_equals(
61 | check_value: Text, expect_value: int, message: Text = ""
62 | ):
63 | """判断长度是否相等"""
64 | assert isinstance(
65 | expect_value, int
66 | ), "expect_value 需要为 int 类型"
67 | assert len(check_value) == expect_value, message
68 |
69 |
70 | def length_greater_than(
71 | check_value: Text, expect_value: Union[int, float], message: Text = ""
72 | ):
73 | """判断长度大于"""
74 | assert isinstance(
75 | expect_value, (float, int)
76 | ), "expect_value 需要为 float/int 类型"
77 | assert len(str(check_value)) > expect_value, message
78 |
79 |
80 | def length_greater_than_or_equals(
81 | check_value: Text, expect_value: Union[int, float], message: Text = ""
82 | ):
83 | """判断长度大于等于"""
84 | assert isinstance(
85 | expect_value, (int, float)
86 | ), "expect_value 需要为 float/int 类型"
87 | assert len(check_value) >= expect_value, message
88 |
89 |
90 | def length_less_than(
91 | check_value: Text, expect_value: Union[int, float], message: Text = ""
92 | ):
93 | """判断长度小于"""
94 | assert isinstance(
95 | expect_value, (int, float)
96 | ), "expect_value 需要为 float/int 类型"
97 | assert len(check_value) < expect_value, message
98 |
99 |
100 | def length_less_than_or_equals(
101 | check_value: Text, expect_value: Union[int, float], message: Text = ""
102 | ):
103 | """判断长度小于等于"""
104 | assert isinstance(
105 | expect_value, (int, float)
106 | ), "expect_value 需要为 float/int 类型"
107 | assert len(check_value) <= expect_value, message
108 |
109 |
110 | def contains(check_value: Any, expect_value: Any, message: Text = ""):
111 | """判断期望结果内容包含在实际结果中"""
112 | assert isinstance(
113 | check_value, (list, tuple, dict, str, bytes)
114 | ), "expect_value 需要为 list/tuple/dict/str/bytes 类型"
115 | assert expect_value in check_value, message
116 |
117 |
118 | def contained_by(check_value: Any, expect_value: Any, message: Text = ""):
119 | """判断实际结果包含在期望结果中"""
120 | assert isinstance(
121 | expect_value, (list, tuple, dict, str, bytes)
122 | ), "expect_value 需要为 list/tuple/dict/str/bytes 类型"
123 |
124 | assert check_value in expect_value, message
125 |
126 |
127 | def startswith(
128 | check_value: Any, expect_value: Any, message: Text = ""
129 | ):
130 | """检查响应内容的开头是否和预期结果内容的开头相等"""
131 | assert str(check_value).startswith(str(expect_value)), message
132 |
133 |
134 | def endswith(
135 | check_value: Any, expect_value: Any, message: Text = ""
136 | ):
137 | """检查响应内容的结尾是否和预期结果内容相等"""
138 | assert str(check_value).endswith(str(expect_value)), message
139 |
--------------------------------------------------------------------------------
/utils/cache_process/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/cache_process/cache_control.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | """
4 | 缓存文件处理
5 | """
6 |
7 | import os
8 | from typing import Any, Text, Union
9 | from common.setting import ensure_path_sep
10 | from utils.other_tools.exceptions import ValueNotFoundError
11 |
12 |
13 | class Cache:
14 | """ 设置、读取缓存 """
15 | def __init__(self, filename: Union[Text, None]) -> None:
16 | # 如果filename不为空,则操作指定文件内容
17 | if filename:
18 | self.path = ensure_path_sep("\\cache" + filename)
19 | # 如果filename为None,则操作所有文件内容
20 | else:
21 | self.path = ensure_path_sep("\\cache")
22 |
23 | def set_cache(self, key: Text, value: Any) -> None:
24 | """
25 | 设置缓存, 只支持设置单字典类型缓存数据, 缓存文件如以存在,则替换之前的缓存内容
26 | :return:
27 | """
28 | with open(self.path, 'w', encoding='utf-8') as file:
29 | file.write(str({key: value}))
30 |
31 | def set_caches(self, value: Any) -> None:
32 | """
33 | 设置多组缓存数据
34 | :param value: 缓存内容
35 | :return:
36 | """
37 | with open(self.path, 'w', encoding='utf-8') as file:
38 | file.write(str(value))
39 |
40 | def get_cache(self) -> Any:
41 | """
42 | 获取缓存数据
43 | :return:
44 | """
45 | try:
46 | with open(self.path, 'r', encoding='utf-8') as file:
47 | return file.read()
48 | except FileNotFoundError:
49 | pass
50 |
51 | def clean_cache(self) -> None:
52 | """删除所有缓存文件"""
53 |
54 | if not os.path.exists(self.path):
55 | raise FileNotFoundError(f"您要删除的缓存文件不存在 {self.path}")
56 | os.remove(self.path)
57 |
58 | @classmethod
59 | def clean_all_cache(cls) -> None:
60 | """
61 | 清除所有缓存文件
62 | :return:
63 | """
64 | cache_path = ensure_path_sep("\\cache")
65 |
66 | # 列出目录下所有文件,生成一个list
67 | list_dir = os.listdir(cache_path)
68 | for i in list_dir:
69 | # 循环删除文件夹下得所有内容
70 | os.remove(cache_path + i)
71 |
72 |
73 | _cache_config = {}
74 |
75 |
76 | class CacheHandler:
77 | @staticmethod
78 | def get_cache(cache_data):
79 | try:
80 | return _cache_config[cache_data]
81 | except KeyError:
82 | raise ValueNotFoundError(f"{cache_data}的缓存数据未找到,请检查是否将该数据存入缓存中")
83 |
84 | @staticmethod
85 | def update_cache(*, cache_name, value):
86 | _cache_config[cache_name] = value
87 |
--------------------------------------------------------------------------------
/utils/cache_process/redis_control.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | """
4 | redis 缓存操作封装
5 | """
6 | from typing import Text, Any
7 | import redis
8 |
9 |
10 | class RedisHandler:
11 | """ redis 缓存读取封装 """
12 |
13 | def __init__(self):
14 | self.host = '127.0.0.0'
15 | self.port = 6379
16 | self.database = 0
17 | self.password = 123456
18 | self.charset = 'UTF-8'
19 | self.redis = redis.Redis(
20 | self.host,
21 | port=self.port,
22 | password=self.password,
23 | decode_responses=True,
24 | db=self.database
25 | )
26 |
27 | def set_string(
28 | self, name: Text,
29 | value, exp_time=None,
30 | exp_milliseconds=None,
31 | name_not_exist=False,
32 | name_exit=False) -> None:
33 | """
34 | 缓存中写入 str(单个)
35 | :param name: 缓存名称
36 | :param value: 缓存值
37 | :param exp_time: 过期时间(秒)
38 | :param exp_milliseconds: 过期时间(毫秒)
39 | :param name_not_exist: 如果设置为True,则只有name不存在时,当前set操作才执行(新增)
40 | :param name_exit: 如果设置为True,则只有name存在时,当前set操作才执行(修改)
41 | :return:
42 | """
43 | self.redis.set(
44 | name,
45 | value,
46 | ex=exp_time,
47 | px=exp_milliseconds,
48 | nx=name_not_exist,
49 | xx=name_exit
50 | )
51 |
52 | def key_exit(self, key: Text):
53 | """
54 | 判断redis中的key是否存在
55 | :param key:
56 | :return:
57 | """
58 |
59 | return self.redis.exists(key)
60 |
61 | def incr(self, key: Text):
62 | """
63 | 使用 incr 方法,处理并发问题
64 | 当 key 不存在时,则会先初始为 0, 每次调用,则会 +1
65 | :return:
66 | """
67 | self.redis.incr(key)
68 |
69 | def get_key(self, name: Any) -> Text:
70 | """
71 | 读取缓存
72 | :param name:
73 | :return:
74 | """
75 | return self.redis.get(name)
76 |
77 | def set_many(self, *args, **kwargs):
78 | """
79 | 批量设置
80 | 支持如下方式批量设置缓存
81 | eg: set_many({'k1': 'v1', 'k2': 'v2'})
82 | set_many(k1="v1", k2="v2")
83 | :return:
84 | """
85 | self.redis.mset(*args, **kwargs)
86 |
87 | def get_many(self, *args):
88 | """获取多个值"""
89 | results = self.redis.mget(*args)
90 | return results
91 |
92 | def del_all_cache(self):
93 | """清理所有现在的数据"""
94 | for key in self.redis.keys():
95 | self.del_cache(key)
96 |
97 | def del_cache(self, name):
98 | """
99 | 删除缓存
100 | :param name:
101 | :return:
102 | """
103 | self.redis.delete(name)
104 |
--------------------------------------------------------------------------------
/utils/logging_tool/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/logging_tool/log_control.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | 日志封装,可设置不同等级的日志颜色
4 | """
5 | import logging
6 | from logging import handlers
7 | from typing import Text
8 | import colorlog
9 | import time
10 | from common.setting import ensure_path_sep
11 |
12 |
13 | class LogHandler:
14 | """ 日志打印封装"""
15 | # 日志级别关系映射
16 | level_relations = {
17 | 'debug': logging.DEBUG,
18 | 'info': logging.INFO,
19 | 'warning': logging.WARNING,
20 | 'error': logging.ERROR,
21 | 'critical': logging.CRITICAL
22 | }
23 |
24 | def __init__(
25 | self,
26 | filename: Text,
27 | level: Text = "info",
28 | when: Text = "D",
29 | fmt: Text = "%(levelname)-8s%(asctime)s%(name)s:%(filename)s:%(lineno)d %(message)s"
30 | ):
31 | self.logger = logging.getLogger(filename)
32 |
33 | formatter = self.log_color()
34 |
35 | # 设置日志格式
36 | format_str = logging.Formatter(fmt)
37 | # 设置日志级别
38 | self.logger.setLevel(self.level_relations.get(level))
39 | # 往屏幕上输出
40 | screen_output = logging.StreamHandler()
41 | # 设置屏幕上显示的格式
42 | screen_output.setFormatter(formatter)
43 | # 往文件里写入#指定间隔时间自动生成文件的处理器
44 | time_rotating = handlers.TimedRotatingFileHandler(
45 | filename=filename,
46 | when=when,
47 | backupCount=3,
48 | encoding='utf-8'
49 | )
50 | # 设置文件里写入的格式
51 | time_rotating.setFormatter(format_str)
52 | # 把对象加到logger里
53 | self.logger.addHandler(screen_output)
54 | self.logger.addHandler(time_rotating)
55 | self.log_path = ensure_path_sep('\\logs\\log.log')
56 |
57 | @classmethod
58 | def log_color(cls):
59 | """ 设置日志颜色 """
60 | log_colors_config = {
61 | 'DEBUG': 'cyan',
62 | 'INFO': 'green',
63 | 'WARNING': 'yellow',
64 | 'ERROR': 'red',
65 | 'CRITICAL': 'red',
66 | }
67 |
68 | formatter = colorlog.ColoredFormatter(
69 | '%(log_color)s[%(asctime)s] [%(name)s] [%(levelname)s]: %(message)s',
70 | log_colors=log_colors_config
71 | )
72 | return formatter
73 |
74 |
75 | now_time_day = time.strftime("%Y-%m-%d", time.localtime())
76 | INFO = LogHandler(ensure_path_sep(f"\\logs\\info-{now_time_day}.log"), level='info')
77 | ERROR = LogHandler(ensure_path_sep(f"\\logs\\error-{now_time_day}.log"), level='error')
78 | WARNING = LogHandler(ensure_path_sep(f'\\logs\\warning-{now_time_day}.log'))
79 |
80 | if __name__ == '__main__':
81 | ERROR.logger.error("测试")
82 |
--------------------------------------------------------------------------------
/utils/logging_tool/log_decorator.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | 日志装饰器,控制程序日志输入,默认为 True
4 | 如设置 False,则程序不会打印日志
5 | """
6 | import ast
7 | from functools import wraps
8 | from utils.read_files_tools.regular_control import cache_regular
9 | from utils.logging_tool.log_control import INFO, ERROR
10 |
11 |
12 | def log_decorator(switch: bool):
13 | """
14 | 封装日志装饰器, 打印请求信息
15 | :param switch: 定义日志开关
16 | :return:
17 | """
18 | def decorator(func):
19 | @wraps(func)
20 | def swapper(*args, **kwargs):
21 |
22 | # 判断日志为开启状态,才打印日志
23 | res = func(*args, **kwargs)
24 | # 判断日志开关为开启状态
25 | if switch:
26 | _log_msg = f"\n======================================================\n" \
27 | f"用例标题: {res.detail}\n" \
28 | f"请求路径: {res.url}\n" \
29 | f"请求方式: {res.method}\n" \
30 | f"请求头: {res.headers}\n" \
31 | f"请求内容: {res.request_body}\n" \
32 | f"接口响应内容: {res.response_data}\n" \
33 | f"接口响应时长: {res.res_time} ms\n" \
34 | f"Http状态码: {res.status_code}\n" \
35 | "====================================================="
36 | _is_run = ast.literal_eval(cache_regular(str(res.is_run)))
37 | # 判断正常打印的日志,控制台输出绿色
38 | if _is_run in (True, None) and res.status_code == 200:
39 | INFO.logger.info(_log_msg)
40 | else:
41 | # 失败的用例,控制台打印红色
42 | ERROR.logger.error(_log_msg)
43 | return res
44 | return swapper
45 | return decorator
46 |
--------------------------------------------------------------------------------
/utils/logging_tool/run_time_decorator.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | 统计请求运行时长装饰器,如请求响应时间超时
4 | 程序中会输入红色日志,提示时间 http 请求超时,默认时长为 3000ms
5 | """
6 | from utils.logging_tool.log_control import ERROR
7 |
8 |
9 | def execution_duration(number: int):
10 | """
11 | 封装统计函数执行时间装饰器
12 | :param number: 函数预计运行时长
13 | :return:
14 | """
15 |
16 | def decorator(func):
17 | def swapper(*args, **kwargs):
18 | res = func(*args, **kwargs)
19 | run_time = res.res_time
20 | # 计算时间戳毫米级别,如果时间大于number,则打印 函数名称 和运行时间
21 | if run_time > number:
22 | ERROR.logger.error(
23 | "\n==============================================\n"
24 | "测试用例执行时间较长,请关注.\n"
25 | "函数运行时间: %s ms\n"
26 | "测试用例相关数据: %s\n"
27 | "================================================="
28 | , run_time, res)
29 | return res
30 | return swapper
31 | return decorator
32 |
--------------------------------------------------------------------------------
/utils/mysql_tool/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/mysql_tool/mysql_control.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | mysql 封装,支持 增、删、改、查
4 | """
5 | import ast
6 | import datetime
7 | import decimal
8 | from warnings import filterwarnings
9 | import pymysql
10 | from typing import List, Union, Text, Dict
11 | from utils import config
12 | from utils.logging_tool.log_control import ERROR
13 | from utils.read_files_tools.regular_control import sql_regular
14 | from utils.read_files_tools.regular_control import cache_regular
15 | from utils.other_tools.exceptions import DataAcquisitionFailed, ValueTypeError
16 |
17 | # 忽略 Mysql 告警信息
18 | filterwarnings("ignore", category=pymysql.Warning)
19 |
20 |
21 | class MysqlDB:
22 | """ mysql 封装 """
23 | if config.mysql_db.switch:
24 |
25 | def __init__(self):
26 |
27 | try:
28 | # 建立数据库连接
29 | self.conn = pymysql.connect(
30 | host=config.mysql_db.host,
31 | user=config.mysql_db.user,
32 | password=config.mysql_db.password,
33 | port=config.mysql_db.port
34 | )
35 |
36 | # 使用 cursor 方法获取操作游标,得到一个可以执行sql语句,并且操作结果为字典返回的游标
37 | self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
38 | except AttributeError as error:
39 | ERROR.logger.error("数据库连接失败,失败原因 %s", error)
40 |
41 | def __del__(self):
42 | try:
43 | # 关闭游标
44 | self.cur.close()
45 | # 关闭连接
46 | self.conn.close()
47 | except AttributeError as error:
48 | ERROR.logger.error("数据库连接失败,失败原因 %s", error)
49 |
50 | def query(self, sql, state="all"):
51 | """
52 | 查询
53 | :param sql:
54 | :param state: all 是默认查询全部
55 | :return:
56 | """
57 | try:
58 | self.cur.execute(sql)
59 |
60 | if state == "all":
61 | # 查询全部
62 | data = self.cur.fetchall()
63 | else:
64 | # 查询单条
65 | data = self.cur.fetchone()
66 | return data
67 | except AttributeError as error_data:
68 | ERROR.logger.error("数据库连接失败,失败原因 %s", error_data)
69 | raise
70 |
71 | def execute(self, sql: Text):
72 | """
73 | 更新 、 删除、 新增
74 | :param sql:
75 | :return:
76 | """
77 | try:
78 | # 使用 execute 操作 sql
79 | rows = self.cur.execute(sql)
80 | # 提交事务
81 | self.conn.commit()
82 | return rows
83 | except AttributeError as error:
84 | ERROR.logger.error("数据库连接失败,失败原因 %s", error)
85 | # 如果事务异常,则回滚数据
86 | self.conn.rollback()
87 | raise
88 |
89 | @classmethod
90 | def sql_data_handler(cls, query_data, data):
91 | """
92 | 处理部分类型sql查询出来的数据格式
93 | @param query_data: 查询出来的sql数据
94 | @param data: 数据池
95 | @return:
96 | """
97 | # 将sql 返回的所有内容全部放入对象中
98 | for key, value in query_data.items():
99 | if isinstance(value, decimal.Decimal):
100 | data[key] = float(value)
101 | elif isinstance(value, datetime.datetime):
102 | data[key] = str(value)
103 | else:
104 | data[key] = value
105 | return data
106 |
107 |
108 | class SetUpMySQL(MysqlDB):
109 | """ 处理前置sql """
110 |
111 | def setup_sql_data(self, sql: Union[List, None]) -> Dict:
112 | """
113 | 处理前置请求sql
114 | :param sql:
115 | :return:
116 | """
117 | sql = ast.literal_eval(cache_regular(str(sql)))
118 | try:
119 | data = {}
120 | if sql is not None:
121 | for i in sql:
122 | # 判断断言类型为查询类型的时候,
123 | if i[0:6].upper() == 'SELECT':
124 | sql_date = self.query(sql=i)[0]
125 | for key, value in sql_date.items():
126 | data[key] = value
127 | else:
128 | self.execute(sql=i)
129 | return data
130 | except IndexError as exc:
131 | raise DataAcquisitionFailed("sql 数据查询失败,请检查setup_sql语句是否正确") from exc
132 |
133 |
134 | class AssertExecution(MysqlDB):
135 | """ 处理断言sql数据 """
136 |
137 | def assert_execution(self, sql: list, resp) -> dict:
138 | """
139 | 执行 sql, 负责处理 yaml 文件中的断言需要执行多条 sql 的场景,最终会将所有数据以对象形式返回
140 | :param resp: 接口响应数据
141 | :param sql: sql
142 | :return:
143 | """
144 | try:
145 | if isinstance(sql, list):
146 |
147 | data = {}
148 | _sql_type = ['UPDATE', 'update', 'DELETE', 'delete', 'INSERT', 'insert']
149 | if any(i in sql for i in _sql_type) is False:
150 | for i in sql:
151 | # 判断sql中是否有正则,如果有则通过jsonpath提取相关的数据
152 | sql = sql_regular(i, resp)
153 | if sql is not None:
154 | # for 循环逐条处理断言 sql
155 | query_data = self.query(sql)[0]
156 | data = self.sql_data_handler(query_data, data)
157 | else:
158 | raise DataAcquisitionFailed(f"该条sql未查询出任何数据, {sql}")
159 | else:
160 | raise DataAcquisitionFailed("断言的 sql 必须是查询的 sql")
161 | else:
162 | raise ValueTypeError("sql数据类型不正确,接受的是list")
163 | return data
164 | except Exception as error_data:
165 | ERROR.logger.error("数据库连接失败,失败原因 %s", error_data)
166 | raise error_data
167 |
168 |
169 | if __name__ == '__main__':
170 | a = MysqlDB()
171 | b = a.query(sql="select * from `test_obp_configure`.lottery_prize where activity_id = 3")
172 | print(b)
173 |
--------------------------------------------------------------------------------
/utils/notify/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/notify/ding_talk.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | 钉钉通知封装
4 | """
5 | import base64
6 | import hashlib
7 | import hmac
8 | import time
9 | import urllib.parse
10 | from typing import Any, Text
11 | from dingtalkchatbot.chatbot import DingtalkChatbot, FeedLink
12 | from utils.other_tools.get_local_ip import get_host_ip
13 | from utils.other_tools.allure_data.allure_report_data import AllureFileClean, TestMetrics
14 | from utils import config
15 |
16 |
17 | class DingTalkSendMsg:
18 | """ 发送钉钉通知 """
19 | def __init__(self, metrics: TestMetrics):
20 | self.metrics = metrics
21 | self.timeStamp = str(round(time.time() * 1000))
22 |
23 | def xiao_ding(self):
24 | sign = self.get_sign()
25 | # 从yaml文件中获取钉钉配置信息
26 | webhook = config.ding_talk.webhook + "×tamp=" + self.timeStamp + "&sign=" + sign
27 | return DingtalkChatbot(webhook)
28 |
29 | def get_sign(self) -> Text:
30 | """
31 | 根据时间戳 + "sign" 生成密钥
32 | :return:
33 | """
34 | string_to_sign = f'{self.timeStamp}\n{config.ding_talk.secret}'.encode('utf-8')
35 | hmac_code = hmac.new(
36 | config.ding_talk.secret.encode('utf-8'),
37 | string_to_sign,
38 | digestmod=hashlib.sha256).digest()
39 |
40 | sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
41 | return sign
42 |
43 | def send_text(
44 | self,
45 | msg: Text,
46 | mobiles=None
47 | ) -> None:
48 | """
49 | 发送文本信息
50 | :param msg: 文本内容
51 | :param mobiles: 艾特用户电话
52 | :return:
53 | """
54 | if not mobiles:
55 | self.xiao_ding().send_text(msg=msg, is_at_all=True)
56 | else:
57 | if isinstance(mobiles, list):
58 | self.xiao_ding().send_text(msg=msg, at_mobiles=mobiles)
59 | else:
60 | raise TypeError("mobiles类型错误 不是list类型.")
61 |
62 | def send_link(
63 | self,
64 | title: Text,
65 | text: Text,
66 | message_url: Text,
67 | pic_url: Text
68 | ) -> None:
69 | """
70 | 发送link通知
71 | :return:
72 | """
73 | self.xiao_ding().send_link(
74 | title=title,
75 | text=text,
76 | message_url=message_url,
77 | pic_url=pic_url
78 | )
79 |
80 | def send_markdown(
81 | self,
82 | title: Text,
83 | msg: Text,
84 | mobiles=None,
85 | is_at_all=False
86 | ) -> None:
87 | """
88 |
89 | :param is_at_all:
90 | :param mobiles:
91 | :param title:
92 | :param msg:
93 | markdown 格式
94 | """
95 |
96 | if mobiles is None:
97 | self.xiao_ding().send_markdown(title=title, text=msg, is_at_all=is_at_all)
98 | else:
99 | if isinstance(mobiles, list):
100 | self.xiao_ding().send_markdown(title=title, text=msg, at_mobiles=mobiles)
101 | else:
102 | raise TypeError("mobiles类型错误 不是list类型.")
103 |
104 | @staticmethod
105 | def feed_link(
106 | title: Text,
107 | message_url: Text,
108 | pic_url: Text
109 | ) -> Any:
110 | """ FeedLink 二次封装 """
111 | return FeedLink(
112 | title=title,
113 | message_url=message_url,
114 | pic_url=pic_url
115 | )
116 |
117 | def send_feed_link(self, *arg) -> None:
118 | """发送 feed_lik """
119 |
120 | self.xiao_ding().send_feed_card(list(arg))
121 |
122 | def send_ding_notification(self):
123 | """ 发送钉钉报告通知 """
124 | # 判断如果有失败的用例,@所有人
125 | is_at_all = False
126 | if self.metrics.failed + self.metrics.broken > 0:
127 | is_at_all = True
128 | text = f"#### {config.project_name}自动化通知 " \
129 | f"\n\n>Python脚本任务: {config.project_name}" \
130 | f"\n\n>环境: TEST\n\n>" \
131 | f"执行人: {config.tester_name}" \
132 | f"\n\n>执行结果: {self.metrics.pass_rate}% " \
133 | f"\n\n>总用例数: {self.metrics.total} " \
134 | f"\n\n>成功用例数: {self.metrics.passed}" \
135 | f" \n\n>失败用例数: {self.metrics.failed} " \
136 | f" \n\n>异常用例数: {self.metrics.broken} " \
137 | f"\n\n>跳过用例数: {self.metrics.skipped}" \
138 | f" \n" \
141 | f" > ###### 测试报告 [详情](http://{get_host_ip()}:9999/index.html) \n"
142 | DingTalkSendMsg(AllureFileClean().get_case_count()).send_markdown(
143 | title="【接口自动化通知】",
144 | msg=text,
145 | is_at_all=is_at_all
146 | )
147 |
148 |
149 | if __name__ == '__main__':
150 | DingTalkSendMsg(AllureFileClean().get_case_count()).send_ding_notification()
151 |
--------------------------------------------------------------------------------
/utils/notify/lark.py:
--------------------------------------------------------------------------------
1 | """
2 | 发送飞书通知
3 | """
4 | import json
5 | import logging
6 | import time
7 | import datetime
8 | import requests
9 | import urllib3
10 | from utils.other_tools.allure_data.allure_report_data import TestMetrics
11 | from utils import config
12 |
13 |
14 | urllib3.disable_warnings()
15 |
16 | try:
17 | JSONDecodeError = json.decoder.JSONDecodeError
18 | except AttributeError:
19 | JSONDecodeError = ValueError
20 |
21 |
22 | def is_not_null_and_blank_str(content):
23 | """
24 | 非空字符串
25 | :param content: 字符串
26 | :return: 非空 - True,空 - False
27 | """
28 | return bool(content and content.strip())
29 |
30 |
31 | class FeiShuTalkChatBot:
32 | """飞书机器人通知"""
33 | def __init__(self, metrics: TestMetrics):
34 | self.metrics = metrics
35 |
36 | def send_text(self, msg: str):
37 | """
38 | 消息类型为text类型
39 | :param msg: 消息内容
40 | :return: 返回消息发送结果
41 | """
42 | data = {"msg_type": "text", "at": {}}
43 | if is_not_null_and_blank_str(msg): # 传入msg非空
44 | data["content"] = {"text": msg}
45 | else:
46 | logging.error("text类型,消息内容不能为空!")
47 | raise ValueError("text类型,消息内容不能为空!")
48 |
49 | logging.debug('text类型:%s', data)
50 | return self.post()
51 |
52 | def post(self):
53 | """
54 | 发送消息(内容UTF-8编码)
55 | :return: 返回消息发送结果
56 | """
57 | rich_text = {
58 | "email": "1603453211@qq.com",
59 | "msg_type": "post",
60 | "content": {
61 | "post": {
62 | "zh_cn": {
63 | "title": "【自动化测试通知】",
64 | "content": [
65 | [
66 | {
67 | "tag": "a",
68 | "text": "测试报告",
69 | "href": "https://192.168.xx.72:8080"
70 | },
71 | {
72 | "tag": "at",
73 | "user_id": "ou_18eac85d35a26f989317ad4f02e8bbbb"
74 | # "text":"陈锐男"
75 | }
76 | ],
77 | [
78 | {
79 | "tag": "text",
80 | "text": "测试 人员 : "
81 | },
82 | {
83 | "tag": "text",
84 | "text": f"{config.tester_name}"
85 | }
86 | ],
87 | [
88 | {
89 | "tag": "text",
90 | "text": "运行 环境 : "
91 | },
92 | {
93 | "tag": "text",
94 | "text": f"{config.env}"
95 | }
96 | ],
97 | [{
98 | "tag": "text",
99 | "text": "成 功 率 : "
100 | },
101 | {
102 | "tag": "text",
103 | "text": f"{self.metrics.pass_rate} %"
104 | }], # 成功率
105 |
106 | [{
107 | "tag": "text",
108 | "text": "成功用例数 : "
109 | },
110 | {
111 | "tag": "text",
112 | "text": f"{self.metrics.passed}"
113 | }], # 成功用例数
114 |
115 | [{
116 | "tag": "text",
117 | "text": "失败用例数 : "
118 | },
119 | {
120 | "tag": "text",
121 | "text": f"{self.metrics.failed}"
122 | }], # 失败用例数
123 | [{
124 | "tag": "text",
125 | "text": "异常用例数 : "
126 | },
127 | {
128 | "tag": "text",
129 | "text": f"{self.metrics.failed}"
130 | }], # 损坏用例数
131 | [
132 | {
133 | "tag": "text",
134 | "text": "时 间 : "
135 | },
136 | {
137 | "tag": "text",
138 | "text": f"{datetime.datetime.now().strftime('%Y-%m-%d')}"
139 | }
140 | ],
141 |
142 | [
143 | {
144 | "tag": "img",
145 | "image_key": "d640eeea-4d2f-4cb3-88d8-c964fab53987",
146 | "width": 300,
147 | "height": 300
148 | }
149 | ]
150 | ]
151 | }
152 | }
153 | }
154 | }
155 | headers = {'Content-Type': 'application/json; charset=utf-8'}
156 |
157 | post_data = json.dumps(rich_text)
158 | response = requests.post(
159 | config.lark.webhook,
160 | headers=headers,
161 | data=post_data,
162 | verify=False
163 | )
164 | result = response.json()
165 |
166 | if result.get('StatusCode') != 0:
167 | time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
168 | result_msg = result['errmsg'] if result.get('errmsg', False) else '未知异常'
169 | error_data = {
170 | "msgtype": "text",
171 | "text": {
172 | "content": f"[注意-自动通知]飞书机器人消息发送失败,时间:{time_now},"
173 | f"原因:{result_msg},请及时跟进,谢谢!"
174 | },
175 | "at": {
176 | "isAtAll": False
177 | }
178 | }
179 | logging.error("消息发送失败,自动通知:%s", error_data)
180 | requests.post(config.lark.webhook, headers=headers, data=json.dumps(error_data))
181 | return result
182 |
--------------------------------------------------------------------------------
/utils/notify/send_mail.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | 描述: 发送邮件
5 | """
6 |
7 | import smtplib
8 | from email.mime.text import MIMEText
9 | from utils.other_tools.allure_data.allure_report_data import TestMetrics, AllureFileClean
10 | from utils import config
11 |
12 |
13 | class SendEmail:
14 | """ 发送邮箱 """
15 | def __init__(self, metrics: TestMetrics):
16 | self.metrics = metrics
17 | self.allure_data = AllureFileClean()
18 | self.CaseDetail = self.allure_data.get_failed_cases_detail()
19 |
20 | @classmethod
21 | def send_mail(cls, user_list: list, sub, content: str) -> None:
22 | """
23 |
24 | @param user_list: 发件人邮箱
25 | @param sub:
26 | @param content: 发送内容
27 | @return:
28 | """
29 | user = "余少琪" + "<" + config.email.send_user + ">"
30 | message = MIMEText(content, _subtype='plain', _charset='utf-8')
31 | message['Subject'] = sub
32 | message['From'] = user
33 | message['To'] = ";".join(user_list)
34 | server = smtplib.SMTP()
35 | server.connect(config.email.email_host)
36 | server.login(config.email.send_user, config.email.stamp_key)
37 | server.sendmail(user, user_list, message.as_string())
38 | server.close()
39 |
40 | def error_mail(self, error_message: str) -> None:
41 | """
42 | 执行异常邮件通知
43 | @param error_message: 报错信息
44 | @return:
45 | """
46 | email = config.email.send_list
47 | user_list = email.split(',') # 多个邮箱发送,config文件中直接添加 '806029174@qq.com'
48 |
49 | sub = config.project_name + "接口自动化执行异常通知"
50 | content = f"自动化测试执行完毕,程序中发现异常,请悉知。报错信息如下:\n{error_message}"
51 | self.send_mail(user_list, sub, content)
52 |
53 | def send_main(self) -> None:
54 | """
55 | 发送邮件
56 | :return:
57 | """
58 | email = config.email.send_list
59 | user_list = email.split(',') # 多个邮箱发送,yaml文件中直接添加 '806029174@qq.com'
60 |
61 | sub = config.project_name + "接口自动化报告"
62 | content = f"""
63 | 各位同事, 大家好:
64 | 自动化用例执行完成,执行结果如下:
65 | 用例运行总数: {self.metrics.total} 个
66 | 通过用例个数: {self.metrics.passed} 个
67 | 失败用例个数: {self.metrics.failed} 个
68 | 异常用例个数: {self.metrics.broken} 个
69 | 跳过用例个数: {self.metrics.skipped} 个
70 | 成 功 率: {self.metrics.pass_rate} %
71 |
72 | {self.allure_data.get_failed_cases_detail()}
73 |
74 | **********************************
75 | jenkins地址:https://121.xx.xx.47:8989/login
76 | 详细情况可登录jenkins平台查看,非相关负责人员可忽略此消息。谢谢。
77 | """
78 | self.send_mail(user_list, sub, content)
79 |
80 |
81 | if __name__ == '__main__':
82 | SendEmail(AllureFileClean().get_case_count()).send_main()
83 |
--------------------------------------------------------------------------------
/utils/notify/wechat_send.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | 描述: 发送企业微信通知
5 | """
6 |
7 | import requests
8 | from utils.logging_tool.log_control import ERROR
9 | from utils.other_tools.allure_data.allure_report_data import TestMetrics, AllureFileClean
10 | from utils.times_tool.time_control import now_time
11 | from utils.other_tools.get_local_ip import get_host_ip
12 | from utils.other_tools.exceptions import SendMessageError, ValueTypeError
13 | from utils import config
14 |
15 |
16 | class WeChatSend:
17 | """
18 | 企业微信消息通知
19 | """
20 |
21 | def __init__(self, metrics: TestMetrics):
22 | self.metrics = metrics
23 | self.headers = {"Content-Type": "application/json"}
24 |
25 | def send_text(self, content, mentioned_mobile_list=None):
26 | """
27 | 发送文本类型通知
28 | :param content: 文本内容,最长不超过2048个字节,必须是utf8编码
29 | :param mentioned_mobile_list: 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
30 | :return:
31 | """
32 | _data = {"msgtype": "text", "text": {"content": content, "mentioned_list": None,
33 | "mentioned_mobile_list": mentioned_mobile_list}}
34 |
35 | if mentioned_mobile_list is None or isinstance(mentioned_mobile_list, list):
36 | # 判断手机号码列表中得数据类型,如果为int类型,发送得消息会乱码
37 | if len(mentioned_mobile_list) >= 1:
38 | for i in mentioned_mobile_list:
39 | if isinstance(i, str):
40 | res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
41 | if res.json()['errcode'] != 0:
42 | ERROR.logger.error(res.json())
43 | raise SendMessageError("企业微信「文本类型」消息发送失败")
44 |
45 | else:
46 | raise ValueTypeError("手机号码必须是字符串类型.")
47 | else:
48 | raise ValueTypeError("手机号码列表必须是list类型.")
49 |
50 | def send_markdown(self, content):
51 | """
52 | 发送 MarkDown 类型消息
53 | :param content: 消息内容,markdown形式
54 | :return:
55 | """
56 | _data = {"msgtype": "markdown", "markdown": {"content": content}}
57 | res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
58 | if res.json()['errcode'] != 0:
59 | ERROR.logger.error(res.json())
60 | raise SendMessageError("企业微信「MarkDown类型」消息发送失败")
61 |
62 | def _upload_file(self, file):
63 | """
64 | 先将文件上传到临时媒体库
65 | """
66 | key = config.wechat.webhook.split("key=")[1]
67 | url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={key}&type=file"
68 | data = {"file": open(file, "rb")}
69 | res = requests.post(url, files=data).json()
70 | return res['media_id']
71 |
72 | def send_file_msg(self, file):
73 | """
74 | 发送文件类型的消息
75 | @return:
76 | """
77 |
78 | _data = {"msgtype": "file", "file": {"media_id": self._upload_file(file)}}
79 | res = requests.post(url=config.wechat.webhook, json=_data, headers=self.headers)
80 | if res.json()['errcode'] != 0:
81 | ERROR.logger.error(res.json())
82 | raise SendMessageError("企业微信「file类型」消息发送失败")
83 |
84 | def send_wechat_notification(self):
85 | """ 发送企业微信通知 """
86 | text = f"""【{config.project_name}自动化通知】
87 | >测试环境:TEST
88 | >测试负责人:@{config.tester_name}
89 | >
90 | > **执行结果**
91 | >成 功 率 : {self.metrics.pass_rate}%
92 | >用例 总数:{self.metrics.total}
93 | >成功用例数:{self.metrics.passed}
94 | >失败用例数:`{self.metrics.failed}个`
95 | >异常用例数:`{self.metrics.broken}个`
96 | >跳过用例数:{self.metrics.skipped}个
97 | >用例执行时长:{self.metrics.time} s
98 | >时间:{now_time()}
99 | >
100 | >非相关负责人员可忽略此消息。
101 | >测试报告,点击查看>>[测试报告入口](http://{get_host_ip()}:9999/index.html)"""
102 |
103 | WeChatSend(AllureFileClean().get_case_count()).send_markdown(text)
104 |
105 |
106 | if __name__ == '__main__':
107 | WeChatSend(AllureFileClean().get_case_count()).send_wechat_notification()
108 |
--------------------------------------------------------------------------------
/utils/other_tools/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/other_tools/__init__.py
--------------------------------------------------------------------------------
/utils/other_tools/address_detection.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from utils.mysql_tool.mysql_control import MysqlDB
3 | import copy
4 |
5 |
6 | class AddressDetection(MysqlDB):
7 |
8 | def get_shop_address_entity_str(self):
9 | """
10 | 获取所有已经上线并且未删除的店铺地址(去除自定店铺,自营店铺没有地址)
11 | :return:
12 | """
13 | shop_info = self.query("SELECT id, name, attribute, shop_type, sub_shop_type "
14 | "FROM `test_obp_supplier`.`supplier_shop` "
15 | "where status = 2 and delete_flag = 0 and sub_shop_type > 300 "
16 | "and sub_shop_type = 300")
17 | return shop_info
18 |
19 | def get_logistics_address_library(self):
20 | """
21 | 获取平台地址库中的省份code
22 | :return:
23 | """
24 |
25 | code = self.query("select name, code from `test_obp_order`.`logistics_address_library` "
26 | "where parent_code > 0")
27 |
28 | area_code = {}
29 | for i in code:
30 | area_code[i['code']] = i['name']
31 | return area_code
32 |
33 | def get_error_shop(self):
34 | """
35 | 获取错误的店铺数据
36 | :return:
37 | """
38 | # 获取区域code
39 | get_logistics_address_library = self.get_logistics_address_library()
40 | num = 0
41 | for i in self.get_shop_address_entity_str():
42 | # 获取店铺地址
43 | shop_address_entity_str = eval(i['attribute'])['shopAddressEntityStr']
44 |
45 | if shop_address_entity_str['countiesName'] == get_logistics_address_library[str(shop_address_entity_str['countiesCode'])]:
46 | pass
47 | else:
48 | area_name = self.query(f"SELECT name, code FROM `test_obp_order`.`logistics_address_library`"
49 | f" where parent_code = {shop_address_entity_str['cityCode']} and name = '{shop_address_entity_str['countiesName']}'")
50 | num += 1
51 |
52 | new_shop_address_entity_str = copy.deepcopy(shop_address_entity_str)
53 | new_shop_address_entity_str['countiesCode'] = area_name[0]['code']
54 | # print(str(f'update obp_supplier.supplier_shop set attribute = json_set(attribute,"$.shopAddressEntityStr.countiesCode",{area_name[0]["code"]}) where id = {i["id"]};'))
55 | print(f"店铺名称: {i['name']}, 店铺id: {i['id']}, "
56 | f"店铺地址:{shop_address_entity_str['cityName']}{shop_address_entity_str['provinceName']}{shop_address_entity_str['countiesName']}"
57 | f"\n当前实际数据:{shop_address_entity_str}"
58 | f"\n{shop_address_entity_str['countiesName']}的实际code码为 {area_name}"
59 | f"\n更改后的数据: {new_shop_address_entity_str}")
60 | print("*" * 100)
61 |
62 |
63 | print(num)
64 |
65 |
66 | AddressDetection().get_error_shop()
67 |
--------------------------------------------------------------------------------
/utils/other_tools/allure_data/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/other_tools/allure_data/allure_report_data.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | 描述: 收集 allure 报告
5 | """
6 |
7 | import json
8 | from typing import List, Text
9 | from common.setting import ensure_path_sep
10 | from utils.read_files_tools.get_all_files_path import get_all_files
11 | from utils.other_tools.models import TestMetrics
12 |
13 |
14 | class AllureFileClean:
15 | """allure 报告数据清洗,提取业务需要得数据"""
16 |
17 | @classmethod
18 | def get_testcases(cls) -> List:
19 | """ 获取所有 allure 报告中执行用例的情况"""
20 | # 将所有数据都收集到files中
21 | files = []
22 | for i in get_all_files(ensure_path_sep("\\report\\html\\data\\test-cases")):
23 | with open(i, 'r', encoding='utf-8') as file:
24 | date = json.load(file)
25 | files.append(date)
26 | return files
27 |
28 | def get_failed_case(self) -> List:
29 | """ 获取到所有失败的用例标题和用例代码路径"""
30 | error_case = []
31 | for i in self.get_testcases():
32 | if i['status'] == 'failed' or i['status'] == 'broken':
33 | error_case.append((i['name'], i['fullName']))
34 | return error_case
35 |
36 | def get_failed_cases_detail(self) -> Text:
37 | """ 返回所有失败的测试用例相关内容 """
38 | date = self.get_failed_case()
39 | values = ""
40 | # 判断有失败用例,则返回内容
41 | if len(date) >= 1:
42 | values = "失败用例:\n"
43 | values += " **********************************\n"
44 | for i in date:
45 | values += " " + i[0] + ":" + i[1] + "\n"
46 | return values
47 |
48 | @classmethod
49 | def get_case_count(cls) -> "TestMetrics":
50 | """ 统计用例数量 """
51 | try:
52 | file_name = ensure_path_sep("\\report\\html\\widgets\\summary.json")
53 | with open(file_name, 'r', encoding='utf-8') as file:
54 | data = json.load(file)
55 | _case_count = data['statistic']
56 | _time = data['time']
57 | keep_keys = {"passed", "failed", "broken", "skipped", "total"}
58 | run_case_data = {k: v for k, v in data['statistic'].items() if k in keep_keys}
59 | # 判断运行用例总数大于0
60 | if _case_count["total"] > 0:
61 | # 计算用例成功率
62 | run_case_data["pass_rate"] = round(
63 | (_case_count["passed"] + _case_count["skipped"]) / _case_count["total"] * 100, 2
64 | )
65 | else:
66 | # 如果未运行用例,则成功率为 0.0
67 | run_case_data["pass_rate"] = 0.0
68 | # 收集用例运行时长
69 | run_case_data['time'] = _time if run_case_data['total'] == 0 else round(_time['duration'] / 1000, 2)
70 | return TestMetrics(**run_case_data)
71 | except FileNotFoundError as exc:
72 | raise FileNotFoundError(
73 | "程序中检查到您未生成allure报告,"
74 | "通常可能导致的原因是allure环境未配置正确,"
75 | "详情可查看如下博客内容:"
76 | "https://blog.csdn.net/weixin_43865008/article/details/124332793"
77 | ) from exc
78 |
79 |
80 | if __name__ == '__main__':
81 | AllureFileClean().get_case_count()
82 |
--------------------------------------------------------------------------------
/utils/other_tools/allure_data/allure_tools.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import json
4 | import allure
5 | from utils.other_tools.models import AllureAttachmentType
6 |
7 |
8 | def allure_step(step: str, var: str) -> None:
9 | """
10 | :param step: 步骤及附件名称
11 | :param var: 附件内容
12 | """
13 | with allure.step(step):
14 | allure.attach(
15 | json.dumps(
16 | str(var),
17 | ensure_ascii=False,
18 | indent=4),
19 | step,
20 | allure.attachment_type.JSON)
21 |
22 |
23 | def allure_attach(source: str, name: str, extension: str):
24 | """
25 | allure报告上传附件、图片、excel等
26 | :param source: 文件路径,相当于传一个文件
27 | :param name: 附件名称
28 | :param extension: 附件的拓展名称
29 | :return:
30 | """
31 | # 获取上传附件的尾缀,判断对应的 attachment_type 枚举值
32 | _name = name.split('.')[-1].upper()
33 | _attachment_type = getattr(AllureAttachmentType, _name, None)
34 |
35 | allure.attach.file(
36 | source=source,
37 | name=name,
38 | attachment_type=_attachment_type if _attachment_type is None else _attachment_type.value,
39 | extension=extension
40 | )
41 |
42 |
43 | def allure_step_no(step: str):
44 | """
45 | 无附件的操作步骤
46 | :param step: 步骤名称
47 | :return:
48 | """
49 | with allure.step(step):
50 | pass
51 |
--------------------------------------------------------------------------------
/utils/other_tools/allure_data/error_case_excel.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import json
5 | import shutil
6 | import ast
7 | import xlwings
8 | from common.setting import ensure_path_sep
9 | from utils.read_files_tools.get_all_files_path import get_all_files
10 | from utils.notify.wechat_send import WeChatSend
11 | from utils.other_tools.allure_data.allure_report_data import AllureFileClean
12 |
13 |
14 | # TODO 还需要处理动态值
15 | class ErrorTestCase:
16 | """ 收集错误的excel """
17 | def __init__(self):
18 | self.test_case_path = ensure_path_sep("\\report\\html\\data\\test-cases\\")
19 |
20 | def get_error_case_data(self):
21 | """
22 | 收集所有失败用例的数据
23 | @return:
24 | """
25 | path = get_all_files(self.test_case_path)
26 | files = []
27 | for i in path:
28 | with open(i, 'r', encoding='utf-8') as file:
29 | date = json.load(file)
30 | # 收集执行失败的用例数据
31 | if date['status'] == 'failed' or date['status'] == 'broken':
32 | files.append(date)
33 | print(files)
34 | return files
35 |
36 | @classmethod
37 | def get_case_name(cls, test_case):
38 | """
39 | 收集测试用例名称
40 | @return:
41 | """
42 | name = test_case['name'].split('[')
43 | case_name = name[1][:-1]
44 | return case_name
45 |
46 | @classmethod
47 | def get_parameters(cls, test_case):
48 | """
49 | 获取allure报告中的 parameters 参数内容, 请求前的数据
50 | 用于兼容用例执行异常,未发送请求导致的情况
51 | @return:
52 | """
53 | parameters = test_case['parameters'][0]['value']
54 | return ast.literal_eval(parameters)
55 |
56 | @classmethod
57 | def get_test_stage(cls, test_case):
58 | """
59 | 获取allure报告中请求后的数据
60 | @return:
61 | """
62 | test_stage = test_case['testStage']['steps']
63 | return test_stage
64 |
65 | def get_case_url(self, test_case):
66 | """
67 | 获取测试用例的 url
68 | @param test_case:
69 | @return:
70 | """
71 | # 判断用例步骤中的数据是否异常
72 | if test_case['testStage']['status'] == 'broken':
73 | # 如果异常状态下,则获取请求前的数据
74 | _url = self.get_parameters(test_case)['url']
75 | else:
76 | # 否则拿请求步骤的数据,因为如果设计到依赖,会获取多组,因此我们只取最后一组数据内容
77 | _url = self.get_test_stage(test_case)[-7]['name'][7:]
78 | return _url
79 |
80 | def get_method(self, test_case):
81 | """
82 | 获取用例中的请求方式
83 | @param test_case:
84 | @return:
85 | """
86 | if test_case['testStage']['status'] == 'broken':
87 | _method = self.get_parameters(test_case)['method']
88 | else:
89 | _method = self.get_test_stage(test_case)[-6]['name'][6:]
90 | return _method
91 |
92 | def get_headers(self, test_case):
93 | """
94 | 获取用例中的请求头
95 | @return:
96 | """
97 | if test_case['testStage']['status'] == 'broken':
98 | _headers = self.get_parameters(test_case)['headers']
99 | else:
100 | # 如果用例请求成功,则从allure附件中获取请求头部信息
101 | _headers_attachment = self.get_test_stage(test_case)[-5]['attachments'][0]['source']
102 | path = ensure_path_sep("\\report\\html\\data\\attachments\\" + _headers_attachment)
103 | with open(path, 'r', encoding='utf-8') as file:
104 | _headers = json.load(file)
105 | return _headers
106 |
107 | def get_request_type(self, test_case):
108 | """
109 | 获取用例的请求类型
110 | @param test_case:
111 | @return:
112 | """
113 | request_type = self.get_parameters(test_case)['requestType']
114 | return request_type
115 |
116 | def get_case_data(self, test_case):
117 | """
118 | 获取用例内容
119 | @return:
120 | """
121 | if test_case['testStage']['status'] == 'broken':
122 | _case_data = self.get_parameters(test_case)['data']
123 | else:
124 | _case_data_attachments = self.get_test_stage(test_case)[-4]['attachments'][0]['source']
125 | path = ensure_path_sep("\\report\\html\\data\\attachments\\" + _case_data_attachments)
126 | with open(path, 'r', encoding='utf-8') as file:
127 | _case_data = json.load(file)
128 | return _case_data
129 |
130 | def get_dependence_case(self, test_case):
131 | """
132 | 获取依赖用例
133 | @param test_case:
134 | @return:
135 | """
136 | _dependence_case_data = self.get_parameters(test_case)['dependence_case_data']
137 | return _dependence_case_data
138 |
139 | def get_sql(self, test_case):
140 | """
141 | 获取 sql 数据
142 | @param test_case:
143 | @return:
144 | """
145 | sql = self.get_parameters(test_case)['sql']
146 | return sql
147 |
148 | def get_assert(self, test_case):
149 | """
150 | 获取断言数据
151 | @param test_case:
152 | @return:
153 | """
154 | assert_data = self.get_parameters(test_case)['assert_data']
155 | return assert_data
156 |
157 | @classmethod
158 | def get_response(cls, test_case):
159 | """
160 | 获取响应内容的数据
161 | @param test_case:
162 | @return:
163 | """
164 | if test_case['testStage']['status'] == 'broken':
165 | _res_date = test_case['testStage']['statusMessage']
166 | else:
167 | try:
168 | res_data_attachments = \
169 | test_case['testStage']['steps'][-1]['attachments'][0]['source']
170 | path = ensure_path_sep("\\report\\html\\data\\attachments\\" + res_data_attachments)
171 | with open(path, 'r', encoding='utf-8') as file:
172 | _res_date = json.load(file)
173 | except FileNotFoundError:
174 | # 程序中没有提取到响应数据,返回None
175 | _res_date = None
176 | return _res_date
177 |
178 | @classmethod
179 | def get_case_time(cls, test_case):
180 | """
181 | 获取用例运行时长
182 | @param test_case:
183 | @return:
184 | """
185 |
186 | case_time = str(test_case['time']['duration']) + "ms"
187 | return case_time
188 |
189 | @classmethod
190 | def get_uid(cls, test_case):
191 | """
192 | 获取 allure 报告中的 uid
193 | @param test_case:
194 | @return:
195 | """
196 | uid = test_case['uid']
197 | return uid
198 |
199 |
200 | class ErrorCaseExcel:
201 | """ 收集运行失败的用例,整理成excel报告 """
202 | def __init__(self):
203 | _excel_template = ensure_path_sep("\\utils\\other_tools\\allure_data\\自动化异常测试用例.xlsx")
204 | self._file_path = ensure_path_sep("\\Files\\" + "自动化异常测试用例.xlsx")
205 | # if os.path.exists(self._file_path):
206 | # os.remove(self._file_path)
207 |
208 | shutil.copyfile(src=_excel_template, dst=self._file_path)
209 | # 打开程序(只打开不新建)
210 | self.app = xlwings.App(visible=False, add_book=False)
211 | self.w_book = self.app.books.open(self._file_path, read_only=False)
212 |
213 | # 选取工作表:
214 | self.sheet = self.w_book.sheets['异常用例'] # 或通过索引选取
215 | self.case_data = ErrorTestCase()
216 |
217 | def background_color(self, position: str, rgb: tuple):
218 | """
219 | excel 单元格设置背景色
220 | @param rgb: rgb 颜色 rgb=(0,255,0)
221 | @param position: 位置,如 A1, B1...
222 | @return:
223 | """
224 | # 定位到单元格位置
225 | rng = self.sheet.range(position)
226 | excel_rgb = rng.color = rgb
227 | return excel_rgb
228 |
229 | def column_width(self, position: str, width: int):
230 | """
231 | 设置列宽
232 | @return:
233 | """
234 | rng = self.sheet.range(position)
235 | # 列宽
236 | excel_column_width = rng.column_width = width
237 | return excel_column_width
238 |
239 | def row_height(self, position, height):
240 | """
241 | 设置行高
242 | @param position:
243 | @param height:
244 | @return:
245 | """
246 | rng = self.sheet.range(position)
247 | excel_row_height = rng.row_height = height
248 | return excel_row_height
249 |
250 | def column_width_adaptation(self, position):
251 | """
252 | excel 所有列宽度自适应
253 | @return:
254 | """
255 | rng = self.sheet.range(position)
256 | auto_fit = rng.columns.autofit()
257 | return auto_fit
258 |
259 | def row_width_adaptation(self, position):
260 | """
261 | excel 设置所有行宽自适应
262 | @return:
263 | """
264 | rng = self.sheet.range(position)
265 | row_adaptation = rng.rows.autofit()
266 | return row_adaptation
267 |
268 | def write_excel_content(self, position: str, value: str):
269 | """
270 | excel 中写入内容
271 | @param value:
272 | @param position:
273 | @return:
274 | """
275 | self.sheet.range(position).value = value
276 |
277 | def write_case(self):
278 | """
279 | 用例中写入失败用例数据
280 | @return:
281 | """
282 |
283 | _data = self.case_data.get_error_case_data()
284 | # 判断有数据才进行写入
285 | if len(_data) > 0:
286 | num = 2
287 | for data in _data:
288 | self.write_excel_content(position="A" + str(num), value=str(self.case_data.get_uid(data)))
289 | self.write_excel_content(position='B' + str(num), value=str(self.case_data.get_case_name(data)))
290 | self.write_excel_content(position="C" + str(num), value=str(self.case_data.get_case_url(data)))
291 | self.write_excel_content(position="D" + str(num), value=str(self.case_data.get_method(data)))
292 | self.write_excel_content(position="E" + str(num), value=str(self.case_data.get_request_type(data)))
293 | self.write_excel_content(position="F" + str(num), value=str(self.case_data.get_headers(data)))
294 | self.write_excel_content(position="G" + str(num), value=str(self.case_data.get_case_data(data)))
295 | self.write_excel_content(position="H" + str(num), value=str(self.case_data.get_dependence_case(data)))
296 | self.write_excel_content(position="I" + str(num), value=str(self.case_data.get_assert(data)))
297 | self.write_excel_content(position="J" + str(num), value=str(self.case_data.get_sql(data)))
298 | self.write_excel_content(position="K" + str(num), value=str(self.case_data.get_case_time(data)))
299 | self.write_excel_content(position="L" + str(num), value=str(self.case_data.get_response(data)))
300 | num += 1
301 | self.w_book.save()
302 | self.w_book.close()
303 | self.app.quit()
304 | # 有数据才发送企业微信
305 | WeChatSend(AllureFileClean().get_case_count()).send_file_msg(self._file_path)
306 |
307 |
308 | if __name__ == '__main__':
309 | ErrorCaseExcel().write_case()
310 |
--------------------------------------------------------------------------------
/utils/other_tools/allure_data/自动化异常测试用例.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/other_tools/allure_data/自动化异常测试用例.xlsx
--------------------------------------------------------------------------------
/utils/other_tools/exceptions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | 描述:
5 | """
6 |
7 |
8 | class MyBaseFailure(Exception):
9 | pass
10 |
11 |
12 | class JsonpathExtractionFailed(MyBaseFailure):
13 | pass
14 |
15 |
16 | class NotFoundError(MyBaseFailure):
17 | pass
18 |
19 |
20 | class FileNotFound(FileNotFoundError, NotFoundError):
21 | pass
22 |
23 |
24 | class SqlNotFound(NotFoundError):
25 | pass
26 |
27 |
28 | class AssertTypeError(MyBaseFailure):
29 | pass
30 |
31 |
32 | class DataAcquisitionFailed(MyBaseFailure):
33 | pass
34 |
35 |
36 | class ValueTypeError(MyBaseFailure):
37 | pass
38 |
39 |
40 | class SendMessageError(MyBaseFailure):
41 | pass
42 |
43 |
44 | class ValueNotFoundError(MyBaseFailure):
45 | pass
46 |
--------------------------------------------------------------------------------
/utils/other_tools/get_local_ip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File :
5 | # @describe:
6 | """
7 |
8 | import socket
9 |
10 |
11 | def get_host_ip():
12 | """
13 | 查询本机ip地址
14 | :return:
15 | """
16 | _s = None
17 | try:
18 | _s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
19 | _s.connect(('8.8.8.8', 80))
20 | l_host = _s.getsockname()[0]
21 | finally:
22 | _s.close()
23 |
24 | return l_host
25 |
--------------------------------------------------------------------------------
/utils/other_tools/install_tool/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/other_tools/install_tool/__init__.py
--------------------------------------------------------------------------------
/utils/other_tools/install_tool/install_requirements.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @Time : 2022/5/10 14:02
5 | # @File : install_requirements
6 | # @describe: 判断程序是否每次会更新依赖库,如有更新,则自动安装
7 | """
8 | import os
9 | import chardet
10 | from common.setting import ensure_path_sep
11 | from utils.logging_tool.log_control import INFO
12 | from utils import config
13 |
14 | os.system("pip3 install chardet")
15 |
16 |
17 | class InstallRequirements:
18 | """ 自动识别安装最新的依赖库 """
19 |
20 | def __init__(self):
21 | self.version_library_comparisons_path = ensure_path_sep("\\utils\\other_tools\\install_tool\\") \
22 | + "version_library_comparisons.txt"
23 | self.requirements_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) \
24 | + os.sep + "requirements.txt"
25 |
26 | self.mirror_url = config.mirror_source
27 | # 初始化时,获取最新的版本库
28 |
29 | # os.system("pip freeze > {0}".format(self.requirements_path))
30 |
31 | def read_version_library_comparisons_txt(self):
32 | """
33 | 获取版本比对默认的文件
34 | @return:
35 | """
36 | with open(self.version_library_comparisons_path, 'r', encoding="utf-8") as file:
37 | return file.read().strip(' ')
38 |
39 | @classmethod
40 | def check_charset(cls, file_path):
41 | """获取文件的字符集"""
42 | with open(file_path, "rb") as file:
43 | data = file.read(4)
44 | charset = chardet.detect(data)['encoding']
45 | return charset
46 |
47 | def read_requirements(self):
48 | """获取安装文件"""
49 | file_data = ""
50 | with open(
51 | self.requirements_path,
52 | 'r',
53 | encoding=self.check_charset(self.requirements_path)
54 | ) as file:
55 |
56 | for line in file:
57 | if "[0m" in line:
58 | line = line.replace("[0m", "")
59 | file_data += line
60 |
61 | with open(
62 | self.requirements_path,
63 | "w",
64 | encoding=self.check_charset(self.requirements_path)
65 | ) as file:
66 | file.write(file_data)
67 |
68 | return file_data
69 |
70 | def text_comparison(self):
71 | """
72 | 版本库比对
73 | @return:
74 | """
75 | read_version_library_comparisons_txt = self.read_version_library_comparisons_txt()
76 | read_requirements = self.read_requirements()
77 | if read_version_library_comparisons_txt == read_requirements:
78 | INFO.logger.info("程序中未检查到更新版本库,已为您跳过自动安装库")
79 | # 程序中如出现不同的文件,则安装
80 | else:
81 | INFO.logger.info("程序中检测到您更新了依赖库,已为您自动安装")
82 | os.system(f"pip3 install -r {self.requirements_path}")
83 | with open(self.version_library_comparisons_path, "w",
84 | encoding=self.check_charset(self.requirements_path)) as file:
85 | file.write(read_requirements)
86 |
87 |
88 | if __name__ == '__main__':
89 | InstallRequirements().text_comparison()
90 |
--------------------------------------------------------------------------------
/utils/other_tools/install_tool/version_library_comparisons.txt:
--------------------------------------------------------------------------------
1 | aiofiles==0.8.0
2 | allure-pytest==2.9.45
3 | allure-python-commons==2.9.45
4 | asgiref==3.5.1
5 | atomicwrites==1.4.0
6 | attrs==21.2.0
7 | blinker==1.4
8 | Brotli==1.0.9
9 | certifi==2021.10.8
10 | cffi==1.15.0
11 | chardet==4.0.0
12 | charset-normalizer==2.0.7
13 | click==8.1.3
14 | colorama==0.4.4
15 | colorlog==6.6.0
16 | cryptography==36.0.0
17 | DingtalkChatbot==1.5.3
18 | et-xmlfile==1.1.0
19 | execnet==1.9.0
20 | Faker==9.8.3
21 | Flask==2.0.3
22 | h11==0.13.0
23 | h2==4.1.0
24 | hpack==4.0.0
25 | httptools==0.4.0
26 | hyperframe==6.0.1
27 | idna==3.3
28 | iniconfig==1.1.0
29 | itchat==1.3.10
30 | itsdangerous==2.1.2
31 | Jinja2==3.1.2
32 | jsonpath==0.82
33 | kaitaistruct==0.9
34 | ldap3==2.9.1
35 | MarkupSafe==2.1.1
36 | mitmproxy==8.0.0
37 | msgpack==1.0.3
38 | multidict==6.0.2
39 | openpyxl==3.0.9
40 | packaging==21.3
41 | passlib==1.7.4
42 | pluggy==1.0.0
43 | protobuf==3.19.4
44 | publicsuffix2==2.20191221
45 | py==1.11.0
46 | pyasn1==0.4.8
47 | pycparser==2.21
48 | pydivert==2.1.0
49 | PyMySQL==1.0.2
50 | pyOpenSSL==21.0.0
51 | pyparsing==3.0.6
52 | pyperclip==1.8.2
53 | pypng==0.0.21
54 | PyQRCode==1.2.1
55 | pytest==6.2.5
56 | pytest-forked==1.3.0
57 | pytest-xdist==2.4.0
58 | python-dateutil==2.8.2
59 | pywin32==304
60 | PyYAML==6.0
61 | requests==2.26.0
62 | requests-toolbelt==0.9.1
63 | ruamel.yaml==0.17.21
64 | ruamel.yaml.clib==0.2.6
65 | sanic==22.3.1
66 | sanic-routing==22.3.0
67 | six==1.16.0
68 | sortedcontainers==2.4.0
69 | text-unidecode==1.3
70 | toml==0.10.2
71 | tornado==6.1
72 | urllib3==1.26.7
73 | urwid==2.1.2
74 | websockets==10.3
75 | Werkzeug==2.1.2
76 | wsproto==1.1.0
77 | xlrd==2.0.1
78 | xlutils==2.0.0
79 | xlwings==0.27.7
80 | xlwt==1.3.0
81 | zstandard==0.17.0
82 |
--------------------------------------------------------------------------------
/utils/other_tools/jsonpath_date_replace.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File : jsonpath_date_replace
5 | # @describe:
6 | """
7 |
8 |
9 | def jsonpath_replace(change_data, key_name, data_switch=None):
10 | """处理jsonpath数据"""
11 | _new_data = key_name + ''
12 | for i in change_data:
13 | if i == '$':
14 | pass
15 | elif data_switch is None and i == "data":
16 | _new_data += '.data'
17 | elif i[0] == '[' and i[-1] == ']':
18 | _new_data += "[" + i[1:-1] + "]"
19 | else:
20 | _new_data += '[' + '"' + i + '"' + "]"
21 | return _new_data
22 |
23 |
24 | if __name__ == '__main__':
25 | jsonpath_replace(change_data=['$', 'data', 'id'], key_name='self.__yaml_case')
26 |
--------------------------------------------------------------------------------
/utils/other_tools/models.py:
--------------------------------------------------------------------------------
1 | import types
2 | from enum import Enum, unique
3 | from typing import Text, Dict, Callable, Union, Optional, List, Any
4 | from dataclasses import dataclass
5 | from pydantic import BaseModel, Field
6 |
7 |
8 | class NotificationType(Enum):
9 | """ 自动化通知方式 """
10 | DEFAULT = 0
11 | DING_TALK = 1
12 | WECHAT = 2
13 | EMAIL = 3
14 | FEI_SHU = 4
15 |
16 |
17 | @dataclass
18 | class TestMetrics:
19 | """ 用例执行数据 """
20 | passed: int
21 | failed: int
22 | broken: int
23 | skipped: int
24 | total: int
25 | pass_rate: float
26 | time: Text
27 |
28 |
29 | class RequestType(Enum):
30 | """
31 | request请求发送,请求参数的数据类型
32 | """
33 | JSON = "JSON"
34 | PARAMS = "PARAMS"
35 | DATA = "DATA"
36 | FILE = 'FILE'
37 | EXPORT = "EXPORT"
38 | NONE = "NONE"
39 |
40 |
41 | def load_module_functions(module) -> Dict[Text, Callable]:
42 | """ 获取 module中方法的名称和所在的内存地址 """
43 | module_functions = {}
44 |
45 | for name, item in vars(module).items():
46 | if isinstance(item, types.FunctionType):
47 | module_functions[name] = item
48 | return module_functions
49 |
50 |
51 | @unique
52 | class DependentType(Enum):
53 | """
54 | 数据依赖相关枚举
55 | """
56 | RESPONSE = 'response'
57 | REQUEST = 'request'
58 | SQL_DATA = 'sqlData'
59 | CACHE = "cache"
60 |
61 |
62 | class Assert(BaseModel):
63 | jsonpath: Text
64 | type: Text
65 | value: Any
66 | AssertType: Union[None, Text] = None
67 |
68 |
69 | class DependentData(BaseModel):
70 | dependent_type: Text
71 | jsonpath: Text
72 | set_cache: Optional[Text]
73 | replace_key: Optional[Text]
74 |
75 |
76 | class DependentCaseData(BaseModel):
77 | case_id: Text
78 | # dependent_data: List[DependentData]
79 | dependent_data: Union[None, List[DependentData]] = None
80 |
81 |
82 | class ParamPrepare(BaseModel):
83 | dependent_type: Text
84 | jsonpath: Text
85 | set_cache: Text
86 |
87 |
88 | class SendRequest(BaseModel):
89 | dependent_type: Text
90 | jsonpath: Optional[Text]
91 | cache_data: Optional[Text]
92 | set_cache: Optional[Text]
93 | replace_key: Optional[Text]
94 |
95 |
96 | class TearDown(BaseModel):
97 | case_id: Text
98 | param_prepare: Optional[List["ParamPrepare"]]
99 | send_request: Optional[List["SendRequest"]]
100 |
101 |
102 | class CurrentRequestSetCache(BaseModel):
103 | type: Text
104 | jsonpath: Text
105 | name: Text
106 |
107 |
108 | class TestCase(BaseModel):
109 | url: Text
110 | method: Text
111 | detail: Text
112 | # assert_data: Union[Dict, Text] = Field(..., alias="assert")
113 | assert_data: Union[Dict, Text]
114 | headers: Union[None, Dict, Text] = {}
115 | requestType: Text
116 | is_run: Union[None, bool, Text] = None
117 | data: Any = None
118 | dependence_case: Union[None, bool] = False
119 | dependence_case_data: Optional[Union[None, List["DependentCaseData"], Text]] = None
120 | sql: List = None
121 | setup_sql: List = None
122 | status_code: Optional[int] = None
123 | teardown_sql: Optional[List] = None
124 | teardown: Union[List["TearDown"], None] = None
125 | current_request_set_cache: Optional[List["CurrentRequestSetCache"]]
126 | sleep: Optional[Union[int, float]]
127 |
128 |
129 | class ResponseData(BaseModel):
130 | url: Text
131 | is_run: Union[None, bool, Text]
132 | detail: Text
133 | response_data: Text
134 | request_body: Any
135 | method: Text
136 | sql_data: Dict
137 | yaml_data: "TestCase"
138 | headers: Dict
139 | cookie: Dict
140 | assert_data: Dict
141 | res_time: Union[int, float]
142 | status_code: int
143 | teardown: List["TearDown"] = None
144 | teardown_sql: Union[None, List]
145 | body: Any
146 |
147 |
148 | class DingTalk(BaseModel):
149 | webhook: Union[Text, None]
150 | secret: Union[Text, None]
151 |
152 |
153 | class MySqlDB(BaseModel):
154 | switch: bool = False
155 | host: Union[Text, None] = None
156 | user: Union[Text, None] = None
157 | password: Union[Text, None] = None
158 | port: Union[int, None] = 3306
159 |
160 |
161 | class Webhook(BaseModel):
162 | webhook: Union[Text, None]
163 |
164 |
165 | class Email(BaseModel):
166 | send_user: Union[Text, None]
167 | email_host: Union[Text, None]
168 | stamp_key: Union[Text, None]
169 | # 收件人
170 | send_list: Union[Text, None]
171 |
172 |
173 | class Config(BaseModel):
174 | project_name: Text
175 | env: Text
176 | tester_name: Text
177 | notification_type: int = 0
178 | excel_report: bool
179 | ding_talk: "DingTalk"
180 | mysql_db: "MySqlDB"
181 | mirror_source: Text
182 | wechat: "Webhook"
183 | email: "Email"
184 | lark: "Webhook"
185 | real_time_update_test_cases: bool = False
186 | host: Text
187 | app_host: Union[Text, None]
188 |
189 |
190 | @unique
191 | class AllureAttachmentType(Enum):
192 | """
193 | allure 报告的文件类型枚举
194 | """
195 | TEXT = "txt"
196 | CSV = "csv"
197 | TSV = "tsv"
198 | URI_LIST = "uri"
199 |
200 | HTML = "html"
201 | XML = "xml"
202 | JSON = "json"
203 | YAML = "yaml"
204 | PCAP = "pcap"
205 |
206 | PNG = "png"
207 | JPG = "jpg"
208 | SVG = "svg"
209 | GIF = "gif"
210 | BMP = "bmp"
211 | TIFF = "tiff"
212 |
213 | MP4 = "mp4"
214 | OGG = "ogg"
215 | WEBM = "webm"
216 |
217 | PDF = "pdf"
218 |
219 |
220 | @unique
221 | class AssertMethod(Enum):
222 | """断言类型"""
223 | equals = "=="
224 | less_than = "lt"
225 | less_than_or_equals = "le"
226 | greater_than = "gt"
227 | greater_than_or_equals = "ge"
228 | not_equals = "not_eq"
229 | string_equals = "str_eq"
230 | length_equals = "len_eq"
231 | length_greater_than = "len_gt"
232 | length_greater_than_or_equals = 'len_ge'
233 | length_less_than = "len_lt"
234 | length_less_than_or_equals = 'len_le'
235 | contains = "contains"
236 | contained_by = 'contained_by'
237 | startswith = 'startswith'
238 | endswith = 'endswith'
239 |
--------------------------------------------------------------------------------
/utils/other_tools/thread_tool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 |
7 | import time
8 | import threading
9 |
10 |
11 | class PyTimer:
12 | """定时器类"""
13 |
14 | def __init__(self, func, *args, **kwargs):
15 | """构造函数"""
16 |
17 | self.func = func
18 | self.args = args
19 | self.kwargs = kwargs
20 | self.running = False
21 |
22 | def _run_func(self):
23 | """运行定时事件函数"""
24 |
25 | _thread = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs)
26 | _thread.setDaemon(True)
27 | _thread.start()
28 |
29 | def _start(self, interval, once):
30 | """启动定时器的线程函数"""
31 |
32 | interval = max(interval, 0.01)
33 |
34 | if interval < 0.050:
35 | _dt = interval / 10
36 | else:
37 | _dt = 0.005
38 |
39 | if once:
40 | deadline = time.time() + interval
41 | while time.time() < deadline:
42 | time.sleep(_dt)
43 |
44 | # 定时时间到,调用定时事件函数
45 | self._run_func()
46 | else:
47 | self.running = True
48 | deadline = time.time() + interval
49 | while self.running:
50 | while time.time() < deadline:
51 | time.sleep(_dt)
52 |
53 | # 更新下一次定时时间
54 | deadline += interval
55 |
56 | # 定时时间到,调用定时事件函数
57 | if self.running:
58 | self._run_func()
59 |
60 | def start(self, interval, once=False):
61 | """启动定时器
62 |
63 | interval - 定时间隔,浮点型,以秒为单位,最高精度10毫秒
64 | once - 是否仅启动一次,默认是连续的
65 | """
66 |
67 | thread_ = threading.Thread(target=self._start, args=(interval, once))
68 | thread_.setDaemon(True)
69 | thread_.start()
70 |
71 | def stop(self):
72 | """停止定时器"""
73 |
74 | self.running = False
75 |
76 |
77 | def do_something(name, gender='male'):
78 | """执行"""
79 | print(time.time(), '定时时间到,执行特定任务')
80 | print('name:%s, gender:%s', name, gender)
81 | time.sleep(5)
82 | print(time.time(), '完成特定任务')
83 |
84 |
85 | timer = PyTimer(do_something, 'Alice', gender='female')
86 | timer.start(0.5, once=False)
87 |
88 | input('按回车键结束\n') # 此处阻塞住进程
89 | timer.stop()
90 |
--------------------------------------------------------------------------------
/utils/read_files_tools/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utils/read_files_tools/case_automatic_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 |
5 | """
6 | import os
7 | from typing import Text, Dict
8 | from common.setting import ensure_path_sep
9 | from utils.read_files_tools.testcase_template import write_testcase_file
10 | from utils.read_files_tools.yaml_control import GetYamlData
11 | from utils.read_files_tools.get_all_files_path import get_all_files
12 | from utils.other_tools.exceptions import ValueNotFoundError
13 |
14 |
15 | class TestCaseAutomaticGeneration:
16 | """自动生成自动化测试中的test_case代码"""
17 |
18 | @staticmethod
19 | def case_date_path() -> Text:
20 | """返回 yaml 用例文件路径"""
21 | return ensure_path_sep("\\data")
22 |
23 | @staticmethod
24 | def case_path() -> Text:
25 | """ 存放用例代码路径"""
26 | return ensure_path_sep("\\test_case")
27 |
28 | def file_name(self, file: Text) -> Text:
29 | """
30 | 通过 yaml文件的命名,将名称转换成 py文件的名称
31 | :param file: yaml 文件路径
32 | :return: 示例: DateDemo.py
33 | """
34 | i = len(self.case_date_path())
35 | yaml_path = file[i:]
36 | file_name = None
37 | # 路径转换
38 | if '.yaml' in yaml_path:
39 | file_name = yaml_path.replace('.yaml', '.py')
40 | elif '.yml' in yaml_path:
41 | file_name = yaml_path.replace('.yml', '.py')
42 | return file_name
43 |
44 | def get_case_path(self, file_path: Text) -> tuple:
45 | """
46 | 根据 yaml 中的用例,生成对应 testCase 层代码的路径
47 | :param file_path: yaml用例路径
48 | :return: D:\\Project\\test_case\\test_case_demo.py, test_case_demo.py
49 | """
50 |
51 | # 这里通过“\\” 符号进行分割,提取出来文件名称
52 | path = self.file_name(file_path).split(os.sep)
53 | # 判断生成的 testcase 文件名称,需要以test_ 开头
54 | case_name = path[-1] = path[-1].replace(path[-1], "test_" + path[-1])
55 | new_name = os.sep.join(path)
56 | return ensure_path_sep("\\test_case" + new_name), case_name
57 |
58 | def get_test_class_title(self, file_path: Text) -> Text:
59 | """
60 | 自动生成类名称
61 | :param file_path:
62 | :return: sup_apply_list --> SupApplyList
63 | """
64 | # 提取文件名称
65 | _file_name = os.path.split(self.file_name(file_path))[1][:-3]
66 | _name = _file_name.split("_")
67 | _name_len = len(_name)
68 | # 将文件名称格式,转换成类名称: sup_apply_list --> SupApplyList
69 | for i in range(_name_len):
70 | _name[i] = _name[i].capitalize()
71 | _class_name = "".join(_name)
72 |
73 | return _class_name
74 |
75 | @staticmethod
76 | def error_message(param_name, file_path):
77 | """
78 | 用例中填写不正确的相关提示
79 | :return:
80 | """
81 | msg = f"用例中未找到 {param_name} 参数值,请检查新增的用例中是否填写对应的参数内容" \
82 | "如已填写,可能是 yaml 参数缩进不正确\n" \
83 | f"用例路径: {file_path}"
84 | return msg
85 |
86 | def func_title(self, file_path: Text) -> Text:
87 | """
88 | 函数名称
89 | :param file_path: yaml 用例路径
90 | :return:
91 | """
92 |
93 | _file_name = os.path.split(self.file_name(file_path))[1][:-3]
94 | return _file_name
95 |
96 | @staticmethod
97 | def allure_epic(case_data: Dict, file_path) -> Text:
98 | """
99 | 用于 allure 报告装饰器中的内容 @allure.epic("项目名称")
100 | :param file_path: 用例路径
101 | :param case_data: 用例数据
102 | :return:
103 | """
104 | try:
105 | return case_data['case_common']['allureEpic']
106 | except KeyError as exc:
107 | raise ValueNotFoundError(TestCaseAutomaticGeneration.error_message(
108 | param_name="allureEpic",
109 | file_path=file_path
110 | )) from exc
111 |
112 | @staticmethod
113 | def allure_feature(case_data: Dict, file_path) -> Text:
114 | """
115 | 用于 allure 报告装饰器中的内容 @allure.feature("模块名称")
116 | :param file_path:
117 | :param case_data:
118 | :return:
119 | """
120 | try:
121 | return case_data['case_common']['allureFeature']
122 | except KeyError as exc:
123 | raise ValueNotFoundError(TestCaseAutomaticGeneration.error_message(
124 | param_name="allureFeature",
125 | file_path=file_path
126 | )) from exc
127 |
128 | @staticmethod
129 | def allure_story(case_data: Dict, file_path) -> Text:
130 | """
131 | 用于 allure 报告装饰器中的内容 @allure.story("测试功能")
132 | :param file_path:
133 | :param case_data:
134 | :return:
135 | """
136 | try:
137 | return case_data['case_common']['allureStory']
138 | except KeyError as exc:
139 | raise ValueNotFoundError(TestCaseAutomaticGeneration.error_message(
140 | param_name="allureStory",
141 | file_path=file_path
142 | )) from exc
143 |
144 | def mk_dir(self, file_path: Text) -> None:
145 | """ 判断生成自动化代码的文件夹路径是否存在,如果不存在,则自动创建 """
146 | # _LibDirPath = os.path.split(self.libPagePath(filePath))[0]
147 |
148 | _case_dir_path = os.path.split(self.get_case_path(file_path)[0])[0]
149 | if not os.path.exists(_case_dir_path):
150 | os.makedirs(_case_dir_path)
151 |
152 | @staticmethod
153 | def case_ids(test_case):
154 | """
155 | 获取用例 ID
156 | :param test_case: 测试用例内容
157 | :return:
158 | """
159 | ids = []
160 | for k, v in test_case.items():
161 | if k != "case_common":
162 | ids.append(k)
163 | return ids
164 |
165 | def yaml_path(self, file_path: Text) -> Text:
166 | """
167 | 生成动态 yaml 路径, 主要处理业务分层场景
168 | :param file_path: 如业务有多个层级, 则获取到每一层/test_demo/DateDemo.py
169 | :return: Login/common.yaml
170 | """
171 | i = len(self.case_date_path())
172 | # 兼容 linux 和 window 操作路径
173 | yaml_path = file_path[i:].replace("\\", "/")
174 | return yaml_path
175 |
176 | def get_case_automatic(self) -> None:
177 | """ 自动生成 测试代码"""
178 | file_path = get_all_files(file_path=ensure_path_sep("\\data"), yaml_data_switch=True)
179 |
180 | for file in file_path:
181 | # 判断代理拦截的yaml文件,不生成test_case代码
182 | if 'proxy_data.yaml' not in file:
183 | # 判断用例需要用的文件夹路径是否存在,不存在则创建
184 | self.mk_dir(file)
185 | yaml_case_process = GetYamlData(file).get_yaml_data()
186 | self.case_ids(yaml_case_process)
187 | write_testcase_file(
188 | allure_epic=self.allure_epic(case_data=yaml_case_process, file_path=file),
189 | allure_feature=self.allure_feature(yaml_case_process, file_path=file),
190 | class_title=self.get_test_class_title(file),
191 | func_title=self.func_title(file),
192 | case_path=self.get_case_path(file)[0],
193 | case_ids=self.case_ids(yaml_case_process),
194 | file_name=self.get_case_path(file)[1],
195 | allure_story=self.allure_story(case_data=yaml_case_process, file_path=file)
196 | )
197 |
198 |
199 | if __name__ == '__main__':
200 | TestCaseAutomaticGeneration().get_case_automatic()
201 |
--------------------------------------------------------------------------------
/utils/read_files_tools/clean_files.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 | import os
7 |
8 |
9 | def del_file(path):
10 | """删除目录下的文件"""
11 | list_path = os.listdir(path)
12 | for i in list_path:
13 | c_path = os.path.join(path, i)
14 | if os.path.isdir(c_path):
15 | del_file(c_path)
16 | else:
17 | os.remove(c_path)
18 |
--------------------------------------------------------------------------------
/utils/read_files_tools/excel_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 | import json
7 |
8 | import xlrd
9 | from xlutils.copy import copy
10 | from common.setting import ensure_path_sep
11 |
12 |
13 | def get_excel_data(sheet_name: str, case_name: any) -> list:
14 | """
15 | 读取 Excel 中的数据
16 | :param sheet_name: excel 中的 sheet 页的名称
17 | :param case_name: 测试用例名称
18 | :return:
19 | """
20 | res_list = []
21 |
22 | excel_dire = ensure_path_sep("\\data\\TestLogin.xls")
23 | work_book = xlrd.open_workbook(excel_dire, formatting_info=True)
24 |
25 | # 打开对应的子表
26 | work_sheet = work_book.sheet_by_name(sheet_name)
27 | # 读取一行
28 | idx = 0
29 | for one in work_sheet.col_values(0):
30 | # 运行需要运行的测试用例
31 | if case_name in one:
32 | req_body_data = work_sheet.cell(idx, 9).value
33 | resp_data = work_sheet.cell(idx, 11).value
34 | res_list.append((req_body_data, json.loads(resp_data)))
35 | idx += 1
36 | return res_list
37 |
38 |
39 | def set_excel_data(sheet_index: int) -> tuple:
40 | """
41 | excel 写入
42 | :return:
43 | """
44 | excel_dire = '../data/TestLogin.xls'
45 | work_book = xlrd.open_workbook(excel_dire, formatting_info=True)
46 | work_book_new = copy(work_book)
47 |
48 | work_sheet_new = work_book_new.get_sheet(sheet_index)
49 | return work_book_new, work_sheet_new
50 |
51 |
52 | if __name__ == '__main__':
53 | get_excel_data("异常用例", '111')
54 |
--------------------------------------------------------------------------------
/utils/read_files_tools/get_all_files_path.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 | import os
6 |
7 |
8 | def get_all_files(file_path, yaml_data_switch=False) -> list:
9 | """
10 | 获取文件路径
11 | :param file_path: 目录路径
12 | :param yaml_data_switch: 是否过滤文件为 yaml格式, True则过滤
13 | :return:
14 | """
15 | filename = []
16 | # 获取所有文件下的子文件名称
17 | for root, dirs, files in os.walk(file_path):
18 | for _file_path in files:
19 | path = os.path.join(root, _file_path)
20 | if yaml_data_switch:
21 | if 'yaml' in path or '.yml' in path:
22 | filename.append(path)
23 | else:
24 | filename.append(path)
25 | return filename
26 |
--------------------------------------------------------------------------------
/utils/read_files_tools/get_yaml_data_analysis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 | from typing import Union, Text, Dict, List
7 | from utils.read_files_tools.yaml_control import GetYamlData
8 | from utils.other_tools.models import TestCase
9 | from utils.other_tools.exceptions import ValueNotFoundError
10 | from utils.cache_process.cache_control import CacheHandler
11 | from utils import config
12 | import os
13 |
14 |
15 | class CaseData:
16 | """
17 | yaml 数据解析, 判断数据填写是否符合规范
18 | """
19 |
20 | def __init__(self, file_path):
21 | self.file_path = file_path
22 |
23 | def __new__(cls, file_path):
24 | if os.path.exists(file_path) is True:
25 | return object.__new__(cls)
26 | else:
27 | raise FileNotFoundError("用例地址未找到")
28 |
29 | def case_process(
30 | self,
31 | case_id_switch: Union[None, bool] = None):
32 | """
33 | 数据清洗之后,返回该 yaml 文件中的所有用例
34 | @param case_id_switch: 判断数据清洗,是否需要清洗出 case_id, 主要用于兼容用例池中的数据
35 | :return:
36 | """
37 | dates = GetYamlData(self.file_path).get_yaml_data()
38 | case_lists = []
39 | for key, values in dates.items():
40 | # 公共配置中的数据,与用例数据不同,需要单独处理
41 | if key != 'case_common':
42 | case_date = {
43 | 'method': self.get_case_method(case_id=key, case_data=values),
44 | 'is_run': self.get_is_run(key, values),
45 | 'url': self.get_case_host(case_id=key, case_data=values),
46 | 'detail': self.get_case_detail(case_id=key, case_data=values),
47 | 'headers': self.get_headers(case_id=key, case_data=values),
48 | 'requestType': self.get_request_type(key, values),
49 | 'data': self.get_case_dates(key, values),
50 | 'dependence_case': self.get_dependence_case(key, values),
51 | 'dependence_case_data': self.get_dependence_case_data(key, values),
52 | "current_request_set_cache": self.get_current_request_set_cache(values),
53 | "sql": self.get_sql(key, values),
54 | "assert_data": self.get_assert(key, values),
55 | "setup_sql": self.setup_sql(values),
56 | "teardown": self.tear_down(values),
57 | "teardown_sql": self.teardown_sql(values),
58 | "sleep": self.time_sleep(values),
59 | }
60 | if case_id_switch is True:
61 | case_lists.append({key: TestCase(**case_date).dict()})
62 | else:
63 | # 正则处理,如果用例中有需要读取缓存中的数据,则优先读取缓存
64 | case_lists.append(TestCase(**case_date).dict())
65 | return case_lists
66 |
67 | def get_case_host(
68 | self, case_id: Text,
69 | case_data: Dict) -> Text:
70 | """
71 | 获取用例的 host
72 | :return:
73 | """
74 | try:
75 | _url = case_data['url']
76 | _host = case_data['host']
77 | if _url is None or _host is None:
78 | raise ValueNotFoundError(
79 | f"用例中的 url 或者 host 不能为空!\n "
80 | f"用例ID: {case_id} \n "
81 | f"用例路径: {self.file_path}"
82 | )
83 | return _host + _url
84 | except KeyError as exc:
85 | raise ValueNotFoundError(
86 | self.raise_value_null_error(data_name="url 或 host", case_id=case_id)
87 | ) from exc
88 |
89 | def get_case_method(
90 | self, case_id: Text,
91 | case_data: Dict) -> Text:
92 | """
93 | 获取用例的请求方式:GET/POST/PUT/DELETE
94 | :return:
95 | """
96 | try:
97 | _case_method = case_data['method']
98 | _request_method = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTION']
99 | if _case_method.upper() not in _request_method:
100 | raise ValueNotFoundError(
101 | f"method 目前只支持 {_request_method} 请求方式,如需新增请联系管理员. "
102 | f"{self.raise_value_error(data_name='请求方式', case_id=case_id, detail=_case_method)}"
103 | )
104 | return _case_method.upper()
105 |
106 | except AttributeError as exc:
107 | raise ValueNotFoundError(
108 | f"method 目前只支持 {['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTION']} 请求方式,"
109 | f"如需新增请联系管理员! "
110 | f"{self.raise_value_error(data_name='请求方式', case_id=case_id, detail=case_data['method'])}"
111 | ) from exc
112 | except KeyError as exc:
113 | raise ValueNotFoundError(
114 | self.raise_value_null_error(data_name="method", case_id=case_id)
115 | ) from exc
116 |
117 | @classmethod
118 | def get_current_request_set_cache(cls, case_data: Dict) -> Dict:
119 | """将当前请求的用例数据存入缓存"""
120 | try:
121 | return case_data['current_request_set_cache']
122 | except KeyError:
123 | ...
124 |
125 | def get_case_detail(
126 | self,
127 | case_id: Text,
128 | case_data: Dict) -> Text:
129 | """
130 | 获取用例描述
131 | :return:
132 | """
133 | try:
134 | return case_data['detail']
135 | except KeyError as exc:
136 | raise ValueNotFoundError(
137 | self.raise_value_null_error(case_id=case_id, data_name="detail")
138 | ) from exc
139 |
140 | def get_headers(
141 | self,
142 | case_id: Text,
143 | case_data: Dict) -> Dict:
144 | """
145 | 胡求用例请求头中的信息
146 | :return:
147 | """
148 | try:
149 | _header = case_data['headers']
150 | return _header
151 | except KeyError as exc:
152 | raise ValueNotFoundError(
153 | self.raise_value_null_error(case_id=case_id, data_name="headers")
154 | ) from exc
155 |
156 | def raise_value_error(
157 | self, data_name: Text,
158 | case_id: Text,
159 | detail: [Text, list, Dict]) -> Text:
160 | """
161 | 所有用例填写不规范的异常提示
162 | :param data_name: 参数名称
163 | :param case_id: 用例ID
164 | :param detail: 参数内容
165 | :return:
166 | """
167 | detail = f"用例中的 {data_name} 填写不正确!\n " \
168 | f"用例ID: {case_id} \n" \
169 | f" 用例路径: {self.file_path}\n" \
170 | f"当前填写的内容: {detail}"
171 |
172 | return detail
173 |
174 | def raise_value_null_error(
175 | self, data_name: Text,
176 | case_id: Text) -> Text:
177 | """
178 | 用例中参数名称为空的异常提示
179 | :param data_name: 参数名称
180 | :param case_id: 用例ID
181 | :return:
182 | """
183 | detail = f"用例中未找到 {data_name} 参数, 如已填写,请检查用例缩进是否存在问题" \
184 | f"用例ID: {case_id} " \
185 | f"用例路径: {self.file_path}"
186 | return detail
187 |
188 | def get_request_type(self, case_id: Text, case_data: Dict) -> Text:
189 | """
190 | 获取请求类型,params、data、json
191 | :return:
192 | """
193 |
194 | _types = ['JSON', 'PARAMS', 'FILE', 'DATA', "EXPORT", "NONE"]
195 |
196 | try:
197 | _request_type = str(case_data['requestType'])
198 | # 判断用户填写的 requestType是否符合规范
199 | if _request_type.upper() not in _types:
200 | raise ValueNotFoundError(
201 | self.raise_value_error(
202 | data_name='requestType',
203 | case_id=case_id,
204 | detail=_request_type
205 | )
206 | )
207 | return _request_type.upper()
208 | # 异常捕捉
209 | except AttributeError as exc:
210 | raise ValueNotFoundError(
211 | self.raise_value_error(
212 | data_name='requestType',
213 | case_id=case_id,
214 | detail=case_data['requestType'])
215 | ) from exc
216 | except KeyError as exc:
217 | raise ValueNotFoundError(
218 | self.raise_value_null_error(case_id=case_id, data_name="requestType")
219 | ) from exc
220 |
221 | def get_is_run(
222 | self,
223 | case_id: Text,
224 | case_data: Dict) -> Text:
225 | """
226 | 获取执行状态, 为 true 或者 None 都会执行
227 | :return:
228 | """
229 | try:
230 | return case_data['is_run']
231 | except KeyError as exc:
232 | raise ValueNotFoundError(
233 | self.raise_value_null_error(case_id=case_id, data_name="is_run")
234 | ) from exc
235 |
236 | def get_dependence_case(
237 | self,
238 | case_id: Text,
239 | case_data: Dict) -> Dict:
240 | """
241 | 获取是否依赖的用例
242 | :return:
243 | """
244 | try:
245 | _dependence_case = case_data['dependence_case']
246 | return _dependence_case
247 | except KeyError as exc:
248 | raise ValueNotFoundError(
249 | self.raise_value_null_error(case_id=case_id, data_name="dependence_case")
250 | ) from exc
251 |
252 | # TODO 对 dependence_case_data 中的值进行验证
253 | def get_dependence_case_data(
254 | self,
255 | case_id: Text,
256 | case_data: Dict) -> Union[Dict, None]:
257 | """
258 | 获取依赖的用例
259 | :return:
260 | """
261 | # 判断如果该用例有依赖,则返回依赖数据,否则返回None
262 | if self.get_dependence_case(case_id=case_id, case_data=case_data):
263 | try:
264 | _dependence_case_data = case_data['dependence_case_data']
265 | # 判断当用例中设置的需要依赖用例,但是dependence_case_data下方没有填写依赖的数据,异常提示
266 | if _dependence_case_data is None:
267 | raise ValueNotFoundError(f"dependence_case_data 依赖数据中缺少依赖相关数据!"
268 | f"如有填写,请检查缩进是否正确"
269 | f"用例ID: {case_id}"
270 | f"用例路径: {self.file_path}")
271 |
272 | return _dependence_case_data
273 | except KeyError as exc:
274 | raise ValueNotFoundError(
275 | self.raise_value_null_error(case_id=case_id, data_name="dependence_case_data")
276 | ) from exc
277 | else:
278 | return None
279 |
280 | def get_case_dates(
281 | self,
282 | case_id: Text,
283 | case_data: Dict) -> Dict:
284 | """
285 | 获取请求数据
286 | :param case_id:
287 | :param case_data:
288 | :return:
289 | """
290 | try:
291 | _dates = case_data['data']
292 | # # 处理请求参数中日期,没有加引号,导致数据不正确问题
293 | # if _dates is not None:
294 | # def data_type(value):
295 | # if isinstance(value, dict):
296 | # for k, v in value.items():
297 | # if isinstance(v, dict):
298 | # data_type(v)
299 | # else:
300 | # if isinstance(v, datetime.date):
301 | # value[k] = str(v)
302 | # data_type(_dates)
303 | return _dates
304 |
305 | except KeyError as exc:
306 | raise ValueNotFoundError(
307 | self.raise_value_null_error(case_id=case_id, data_name="data")
308 | ) from exc
309 |
310 | # TODO 对 assert 中的值进行验证
311 | def get_assert(
312 | self,
313 | case_id: Text,
314 | case_data: Dict):
315 | """
316 | 获取需要断言的数据
317 | :return:
318 | """
319 | try:
320 | _assert = case_data['assert']
321 | if _assert is None:
322 | raise self.raise_value_error(data_name="assert", case_id=case_id, detail=_assert)
323 | return case_data['assert']
324 | except KeyError as exc:
325 | raise ValueNotFoundError(
326 | self.raise_value_null_error(case_id=case_id, data_name="assert")
327 | ) from exc
328 |
329 | def get_sql(
330 | self,
331 | case_id: Text,
332 | case_data: Dict) -> Union[list, None]:
333 | """
334 | 获取测试用例中的断言sql
335 | :return:
336 | """
337 | try:
338 | _sql = case_data['sql']
339 | # 判断数据库开关为开启状态,并且sql不为空
340 | if config.mysql_db.switch and _sql is None:
341 | return None
342 | return case_data['sql']
343 | except KeyError as exc:
344 | raise ValueNotFoundError(
345 | self.raise_value_null_error(case_id=case_id, data_name="sql")
346 | ) from exc
347 |
348 | @classmethod
349 | def setup_sql(cls, case_data: Dict) -> Union[list, None]:
350 | """
351 | 获取前置sql,比如该条用例中需要从数据库中读取sql作为用例参数,则需填写setup_sql
352 | :return:
353 | """
354 | try:
355 | _setup_sql = case_data['setup_sql']
356 | return _setup_sql
357 | except KeyError:
358 | return None
359 |
360 | @classmethod
361 | def tear_down(cls, case_data: Dict) -> Union[Dict, None]:
362 | """
363 | 获取后置请求数据
364 | """
365 | try:
366 | _teardown = case_data['teardown']
367 | return _teardown
368 | except KeyError:
369 | return None
370 |
371 | @classmethod
372 | def teardown_sql(cls, case_data: Dict) -> Union[list, None]:
373 | """
374 | 获取前置sql,比如该条用例中需要从数据库中读取sql作为用例参数,则需填写setup_sql
375 | :return:
376 | """
377 | try:
378 | _teardown_sql = case_data['teardown_sql']
379 | return _teardown_sql
380 | except KeyError:
381 | return None
382 |
383 | @classmethod
384 | def time_sleep(cls, case_data: Dict) -> Union[int, float, None]:
385 | """ 设置休眠时间 """
386 | try:
387 | _sleep_time = case_data['sleep']
388 | return _sleep_time
389 | except KeyError:
390 | return None
391 |
392 |
393 | class GetTestCase:
394 |
395 | @staticmethod
396 | def case_data(case_id_lists: List):
397 | case_lists = []
398 | for i in case_id_lists:
399 | _data = CacheHandler.get_cache(i)
400 | case_lists.append(_data)
401 |
402 | return case_lists
403 |
404 |
405 | if __name__ == '__main__':
406 | a = CaseData(r'D:\work_code\pytest-auto-api2\data\Collect\collect_addtool.yaml').case_process()
407 | print(a)
408 |
--------------------------------------------------------------------------------
/utils/read_files_tools/regular_control.py:
--------------------------------------------------------------------------------
1 | """
2 | Desc : 自定义函数调用
3 |
4 | """
5 | import re
6 | import datetime
7 | import random
8 | from datetime import date, timedelta, datetime
9 | from jsonpath import jsonpath
10 | from faker import Faker
11 | from utils.logging_tool.log_control import ERROR
12 |
13 |
14 | class Context:
15 | """ 正则替换 """
16 | def __init__(self):
17 | self.faker = Faker(locale='zh_CN')
18 |
19 | @classmethod
20 | def random_int(cls) -> int:
21 | """
22 | :return: 随机数
23 | """
24 | _data = random.randint(0, 5000)
25 | return _data
26 |
27 | def get_phone(self) -> int:
28 | """
29 | :return: 随机生成手机号码
30 | """
31 | phone = self.faker.phone_number()
32 | return phone
33 |
34 | def get_id_number(self) -> int:
35 | """
36 |
37 | :return: 随机生成身份证号码
38 | """
39 |
40 | id_number = self.faker.ssn()
41 | return id_number
42 |
43 | def get_female_name(self) -> str:
44 | """
45 |
46 | :return: 女生姓名
47 | """
48 | female_name = self.faker.name_female()
49 | return female_name
50 |
51 | def get_male_name(self) -> str:
52 | """
53 |
54 | :return: 男生姓名
55 | """
56 | male_name = self.faker.name_male()
57 | return male_name
58 |
59 | def get_email(self) -> str:
60 | """
61 |
62 | :return: 生成邮箱
63 | """
64 | email = self.faker.email()
65 | return email
66 |
67 | @classmethod
68 | def self_operated_id(cls):
69 | """自营店铺 ID """
70 | operated_id = 212
71 | return operated_id
72 |
73 | @classmethod
74 | def get_time(cls) -> str:
75 | """
76 | 计算当前时间
77 | :return:
78 | """
79 | now_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
80 | return now_time
81 |
82 | @classmethod
83 | def today_date(cls):
84 | """获取今日0点整时间"""
85 |
86 | _today = date.today().strftime("%Y-%m-%d") + " 00:00:00"
87 | return str(_today)
88 |
89 | @classmethod
90 | def time_after_week(cls):
91 | """获取一周后12点整的时间"""
92 |
93 | _time_after_week = (date.today() + timedelta(days=+6)).strftime("%Y-%m-%d") + " 00:00:00"
94 | return _time_after_week
95 |
96 | @classmethod
97 | def host(cls) -> str:
98 | from utils import config
99 | """ 获取接口域名 """
100 | return config.host
101 |
102 | @classmethod
103 | def app_host(cls) -> str:
104 | from utils import config
105 | """获取app的host"""
106 | return config.app_host
107 |
108 |
109 | def sql_json(js_path, res):
110 | """ 提取 sql中的 json 数据 """
111 | _json_data = jsonpath(res, js_path)[0]
112 | if _json_data is False:
113 | raise ValueError(f"sql中的jsonpath获取失败 {res}, {js_path}")
114 | return jsonpath(res, js_path)[0]
115 |
116 |
117 | def sql_regular(value, res=None):
118 | """
119 | 这里处理sql中的依赖数据,通过获取接口响应的jsonpath的值进行替换
120 | :param res: jsonpath使用的返回结果
121 | :param value:
122 | :return:
123 | """
124 | sql_json_list = re.findall(r"\$json\((.*?)\)\$", value)
125 |
126 | for i in sql_json_list:
127 | pattern = re.compile(r'\$json\(' + i.replace('$', "\$").replace('[', '\[') + r'\)\$')
128 | key = str(sql_json(i, res))
129 | value = re.sub(pattern, key, value, count=1)
130 |
131 | return value
132 |
133 |
134 | def cache_regular(value):
135 | from utils.cache_process.cache_control import CacheHandler
136 |
137 | """
138 | 通过正则的方式,读取缓存中的内容
139 | 例:$cache{login_init}
140 | :param value:
141 | :return:
142 | """
143 | # 正则获取 $cache{login_init}中的值 --> login_init
144 | regular_dates = re.findall(r"\$cache\{(.*?)\}", value)
145 |
146 | # 拿到的是一个list,循环数据
147 | for regular_data in regular_dates:
148 | value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
149 | if any(i in regular_data for i in value_types) is True:
150 | value_types = regular_data.split(":")[0]
151 | regular_data = regular_data.split(":")[1]
152 | # pattern = re.compile(r'\'\$cache{' + value_types.split(":")[0] + r'(.*?)}\'')
153 | pattern = re.compile(r'\'\$cache\{' + value_types.split(":")[0] + ":" + regular_data + r'\}\'')
154 | else:
155 | pattern = re.compile(
156 | r'\$cache\{' + regular_data.replace('$', "\$").replace('[', '\[') + r'\}'
157 | )
158 | try:
159 | # cache_data = Cache(regular_data).get_cache()
160 | cache_data = CacheHandler.get_cache(regular_data)
161 | # 使用sub方法,替换已经拿到的内容
162 | value = re.sub(pattern, str(cache_data), value)
163 | except Exception:
164 | pass
165 | return value
166 |
167 |
168 | def regular(target):
169 | """
170 | 新版本
171 | 使用正则替换请求数据
172 | :return:
173 | """
174 | try:
175 | regular_pattern = r'\${{(.*?)}}'
176 | while re.findall(regular_pattern, target):
177 | key = re.search(regular_pattern, target).group(1)
178 | value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
179 | if any(i in key for i in value_types) is True:
180 | func_name = key.split(":")[1].split("(")[0]
181 | value_name = key.split(":")[1].split("(")[1][:-1]
182 | if value_name == "":
183 | value_data = getattr(Context(), func_name)()
184 | else:
185 | value_data = getattr(Context(), func_name)(*value_name.split(","))
186 | regular_int_pattern = r'\'\${{(.*?)}}\''
187 | target = re.sub(regular_int_pattern, str(value_data), target, 1)
188 | else:
189 | func_name = key.split("(")[0]
190 | value_name = key.split("(")[1][:-1]
191 | if value_name == "":
192 | value_data = getattr(Context(), func_name)()
193 | else:
194 | value_data = getattr(Context(), func_name)(*value_name.split(","))
195 | target = re.sub(regular_pattern, str(value_data), target, 1)
196 | return target
197 |
198 | except AttributeError:
199 | ERROR.logger.error("未找到对应的替换的数据, 请检查数据是否正确 %s", target)
200 | raise
201 | except IndexError:
202 | ERROR.logger.error("yaml中的 ${{}} 函数方法不正确,正确语法实例:${{get_time()}}")
203 | raise
204 |
205 |
206 | if __name__ == '__main__':
207 | a = "${{host()}} aaa"
208 | b = regular(a)
209 |
--------------------------------------------------------------------------------
/utils/read_files_tools/swagger_for_yaml.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 |
5 | """
6 | import json
7 | from jsonpath import jsonpath
8 | from common.setting import ensure_path_sep
9 | from typing import Dict
10 | from ruamel import yaml
11 | import os
12 |
13 |
14 | class SwaggerForYaml:
15 | def __init__(self):
16 | self._data = self.get_swagger_json()
17 |
18 | @classmethod
19 | def get_swagger_json(cls):
20 | """
21 | 获取 swagger 中的 json 数据
22 | :return:
23 | """
24 | try:
25 | with open('./file/test_OpenAPI.json', "r", encoding='utf-8') as f:
26 | row_data = json.load(f)
27 | return row_data
28 | except FileNotFoundError:
29 | raise FileNotFoundError("文件路径不存在,请重新输入")
30 |
31 | def get_allure_epic(self):
32 | """ 获取 yaml 用例中的 allure_epic """
33 | _allure_epic = self._data['info']['title']
34 | return _allure_epic
35 |
36 | @classmethod
37 | def get_allure_feature(cls, value):
38 | """ 获取 yaml 用例中的 allure_feature """
39 | _allure_feature = value['tags']
40 | return str(_allure_feature)
41 |
42 | @classmethod
43 | def get_allure_story(cls, value):
44 | """ 获取 yaml 用例中的 allure_story """
45 | _allure_story = value['summary']
46 | return _allure_story
47 |
48 | @classmethod
49 | def get_case_id(cls, value):
50 | """ 获取 case_id """
51 | _case_id = value.replace("/", "_")
52 | return "01" + _case_id
53 |
54 | @classmethod
55 | def get_detail(cls, value):
56 | _get_detail = value['summary']
57 | return "测试" + _get_detail
58 |
59 | @classmethod
60 | def get_request_type(cls, value, headers):
61 | """ 处理 request_type """
62 | if jsonpath(obj=value, expr="$.parameters") is not False:
63 | _parameters = value['parameters']
64 | if _parameters[0]['in'] == 'query':
65 | return "params"
66 | else:
67 | if 'application/x-www-form-urlencoded' or 'multipart/form-data' in headers:
68 | return "data"
69 | elif 'application/json' in headers:
70 | return "json"
71 | elif 'application/octet-stream' in headers:
72 | return "file"
73 | else:
74 | return "data"
75 |
76 | @classmethod
77 | def get_case_data(cls, value):
78 | """ 处理 data 数据 """
79 | _dict = {}
80 | if jsonpath(obj=value, expr="$.parameters") is not False:
81 | _parameters = value['parameters']
82 | for i in _parameters:
83 | if i['in'] == 'header':
84 | ...
85 | else:
86 | _dict[i['name']] = None
87 | else:
88 | return None
89 | return _dict
90 |
91 | @classmethod
92 | def yaml_cases(cls, data: Dict, file_path: str) -> None:
93 | """
94 | 写入 yaml 数据
95 | :param file_path:
96 | :param data: 测试用例数据
97 | :return:
98 | """
99 |
100 | _file_path = ensure_path_sep("\\data\\" + file_path[1:].replace("/", os.sep) + '.yaml')
101 | _file = _file_path.split(os.sep)[:-1]
102 | _dir_path = ''
103 | for i in _file:
104 | _dir_path += i + os.sep
105 | try:
106 | os.makedirs(_dir_path)
107 | except FileExistsError:
108 | ...
109 | with open(_file_path, "a", encoding="utf-8") as file:
110 | yaml.dump(data, file, Dumper=yaml.RoundTripDumper, allow_unicode=True)
111 | file.write('\n')
112 |
113 | @classmethod
114 | def get_headers(cls, value):
115 | """ 获取请求头 """
116 | _headers = {}
117 | if jsonpath(obj=value, expr="$.consumes") is not False:
118 | _headers = {"Content-Type": value['consumes'][0]}
119 | if jsonpath(obj=value, expr="$.parameters") is not False:
120 | for i in value['parameters']:
121 | if i['in'] == 'header':
122 | _headers[i['name']] = None
123 | else:
124 | _headers = None
125 | return _headers
126 |
127 | def write_yaml_handler(self):
128 |
129 | _api_data = self._data['paths']
130 | for key, value in _api_data.items():
131 | for k, v in value.items():
132 | yaml_data = {
133 | "case_common": {"allureEpic": self.get_allure_epic(), "allureFeature": self.get_allure_feature(v),
134 | "allureStory": self.get_allure_story(v)},
135 | self.get_case_id(key): {
136 | "host": "${{host()}}", "url": key, "method": k, "detail": self.get_detail(v),
137 | "headers": self.get_headers(v), "requestType": self.get_request_type(v, self.get_headers(v)),
138 | "is_run": None, "data": self.get_case_data(v), "dependence_case": False,
139 | "assert": {"status_code": 200}, "sql": None}}
140 | self.yaml_cases(yaml_data, file_path=key)
141 |
142 |
143 | if __name__ == '__main__':
144 | SwaggerForYaml().write_yaml_handler()
145 |
--------------------------------------------------------------------------------
/utils/read_files_tools/testcase_template.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File : testcase_template
5 | # @describe: 用例模板
6 | """
7 |
8 | import datetime
9 | import os
10 | from utils.read_files_tools.yaml_control import GetYamlData
11 | from common.setting import ensure_path_sep
12 | from utils.other_tools.exceptions import ValueNotFoundError
13 |
14 |
15 | def write_case(case_path, page):
16 | """ 写入用例数据 """
17 | with open(case_path, 'w', encoding="utf-8") as file:
18 | file.write(page)
19 |
20 |
21 | def write_testcase_file(*, allure_epic, allure_feature, class_title,
22 | func_title, case_path, case_ids, file_name, allure_story):
23 | """
24 |
25 | :param allure_story:
26 | :param file_name: 文件名称
27 | :param allure_epic: 项目名称
28 | :param allure_feature: 模块名称
29 | :param class_title: 类名称
30 | :param func_title: 函数名称
31 | :param case_path: case 路径
32 | :param case_ids: 用例ID
33 | :return:
34 | """
35 | conf_data = GetYamlData(ensure_path_sep("\\common\\config.yaml")).get_yaml_data()
36 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
37 | real_time_update_test_cases = conf_data['real_time_update_test_cases']
38 |
39 | page = f'''#!/usr/bin/env python
40 | # -*- coding: utf-8 -*-
41 | # @Time : {now}
42 |
43 |
44 | import allure
45 | import pytest
46 | from utils.read_files_tools.get_yaml_data_analysis import GetTestCase
47 | from utils.assertion.assert_control import Assert
48 | from utils.requests_tool.request_control import RequestControl
49 | from utils.read_files_tools.regular_control import regular
50 | from utils.requests_tool.teardown_control import TearDownHandler
51 |
52 |
53 | case_id = {case_ids}
54 | TestData = GetTestCase.case_data(case_id)
55 | re_data = regular(str(TestData))
56 |
57 |
58 | @allure.epic("{allure_epic}")
59 | @allure.feature("{allure_feature}")
60 | class Test{class_title}:
61 |
62 | @allure.story("{allure_story}")
63 | @pytest.mark.parametrize('in_data', eval(re_data), ids=[i['detail'] for i in TestData])
64 | def test_{func_title}(self, in_data, case_skip):
65 | """
66 | :param :
67 | :return:
68 | """
69 | res = RequestControl(in_data).http_request()
70 | TearDownHandler(res).teardown_handle()
71 | Assert(in_data['assert_data']).assert_equality(response_data=res.response_data,
72 | sql_data=res.sql_data, status_code=res.status_code)
73 |
74 |
75 | if __name__ == '__main__':
76 | pytest.main(['{file_name}', '-s', '-W', 'ignore:Module already imported:pytest.PytestWarning'])
77 | '''
78 | if real_time_update_test_cases:
79 | write_case(case_path=case_path, page=page)
80 | elif real_time_update_test_cases is False:
81 | if not os.path.exists(case_path):
82 | write_case(case_path=case_path, page=page)
83 | else:
84 | raise ValueNotFoundError("real_time_update_test_cases 配置不正确,只能配置 True 或者 False")
85 |
--------------------------------------------------------------------------------
/utils/read_files_tools/yaml_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 | import os
7 | import ast
8 | import yaml.scanner
9 | from utils.read_files_tools.regular_control import regular
10 |
11 |
12 | class GetYamlData:
13 | """ 获取 yaml 文件中的数据 """
14 |
15 | def __init__(self, file_dir):
16 | self.file_dir = str(file_dir)
17 |
18 | def get_yaml_data(self) -> dict:
19 | """
20 | 获取 yaml 中的数据
21 | :param: fileDir:
22 | :return:
23 | """
24 | # 判断文件是否存在
25 | if os.path.exists(self.file_dir):
26 | data = open(self.file_dir, 'r', encoding='utf-8')
27 | res = yaml.load(data, Loader=yaml.FullLoader)
28 | else:
29 | raise FileNotFoundError("文件路径不存在")
30 | return res
31 |
32 | def write_yaml_data(self, key: str, value) -> int:
33 | """
34 | 更改 yaml 文件中的值, 并且保留注释内容
35 | :param key: 字典的key
36 | :param value: 写入的值
37 | :return:
38 | """
39 | with open(self.file_dir, 'r', encoding='utf-8') as file:
40 | # 创建了一个空列表,里面没有元素
41 | lines = []
42 | for line in file.readlines():
43 | if line != '\n':
44 | lines.append(line)
45 | file.close()
46 |
47 | with open(self.file_dir, 'w', encoding='utf-8') as file:
48 | flag = 0
49 | for line in lines:
50 | left_str = line.split(":")[0]
51 | if key == left_str and '#' not in line:
52 | newline = f"{left_str}: {value}"
53 | line = newline
54 | file.write(f'{line}\n')
55 | flag = 1
56 | else:
57 | file.write(f'{line}')
58 | file.close()
59 | return flag
60 |
61 |
62 | class GetCaseData(GetYamlData):
63 | """ 获取测试用例中的数据 """
64 |
65 | def get_different_formats_yaml_data(self) -> list:
66 | """
67 | 获取兼容不同格式的yaml数据
68 | :return:
69 | """
70 | res_list = []
71 | for i in self.get_yaml_data():
72 | res_list.append(i)
73 | return res_list
74 |
75 | def get_yaml_case_data(self):
76 | """
77 | 获取测试用例数据, 转换成指定数据格式
78 | :return:
79 | """
80 |
81 | _yaml_data = self.get_yaml_data()
82 | # 正则处理yaml文件中的数据
83 | re_data = regular(str(_yaml_data))
84 | return ast.literal_eval(re_data)
85 |
--------------------------------------------------------------------------------
/utils/recording/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/recording/__init__.py
--------------------------------------------------------------------------------
/utils/recording/mitmproxy_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 | from urllib.parse import parse_qs, urlparse
6 | from typing import Any, Union, Text, List, Dict, Tuple
7 | import ast
8 | import os
9 | import mitmproxy.http
10 | from mitmproxy import ctx
11 | from ruamel import yaml
12 |
13 |
14 | class Counter:
15 | """
16 | 代理录制,基于 mitmproxy 库拦截获取网络请求
17 | 将接口请求数据转换成 yaml 测试用例
18 | 参考资料: https://blog.wolfogre.com/posts/usage-of-mitmproxy/
19 | """
20 |
21 | def __init__(self, filter_url: List, filename: Text = './data/proxy_data.yaml'):
22 | self.num = 0
23 | self.file = filename
24 | self.counter = 1
25 | # 需要过滤的 url
26 | self.url = filter_url
27 |
28 | def response(self, flow: mitmproxy.http.HTTPFlow) -> None:
29 | """
30 | mitmproxy抓包处理响应,在这里汇总需要数据, 过滤 包含指定url,并且响应格式是 json的
31 | :param flow:
32 | :return:
33 | """
34 | # 存放需要过滤的接口
35 | filter_url_type = ['.css', '.js', '.map', '.ico', '.png', '.woff', '.map3', '.jpeg', '.jpg']
36 | url = flow.request.url
37 | ctx.log.info("=" * 100)
38 | # 判断过滤掉含 filter_url_type 中后缀的 url
39 | if any(i in url for i in filter_url_type) is False:
40 | # 存放测试用例
41 | if self.filter_url(url):
42 |
43 | data = self.data_handle(flow.request.text)
44 | method = flow.request.method
45 | header = self.token_handle(flow.request.headers)
46 | response = flow.response.text
47 | case_id = self.get_case_id(url) + str(self.counter)
48 | cases = {
49 | case_id: {
50 | "host": self.host_handle(url),
51 | "url": self.url_path_handle(url),
52 | "method": method,
53 | "detail": None,
54 | "headers": header,
55 | 'requestType': self.request_type_handler(method),
56 | "is_run": True,
57 | "data": data,
58 | "dependence_case": None,
59 | "dependence_case_data": None,
60 | "assert": self.response_code_handler(response),
61 | "sql": None
62 | }
63 | }
64 | # 判断如果请求参数时拼接在url中,提取url中参数,转换成字典
65 | if "?" in url:
66 | cases[case_id]['url'] = self.get_url_handler(url)[1]
67 | cases[case_id]['data'] = self.get_url_handler(url)[0]
68 |
69 | ctx.log.info("=" * 100)
70 | ctx.log.info(cases)
71 |
72 | # 判断文件不存在则创建文件
73 | try:
74 | self.yaml_cases(cases)
75 | except FileNotFoundError:
76 | os.makedirs(self.file)
77 | self.counter += 1
78 |
79 | @classmethod
80 | def get_case_id(cls, url: Text) -> Text:
81 | """
82 | 通过url,提取对应的user_id
83 | :param url:
84 | :return:
85 | """
86 | _url_path = str(url).split('?')[0]
87 | # 通过url中的接口地址,最后一个参数,作为case_id的名称
88 | _url = _url_path.split('/')
89 | return _url[-1]
90 |
91 | def filter_url(self, url: Text) -> bool:
92 | """过滤url"""
93 | for i in self.url:
94 | # 判断当前拦截的url地址,是否是addons中配置的host
95 | if i in url:
96 | # 如果是,则返回True
97 | return True
98 | # 否则返回 False
99 | return False
100 |
101 | @classmethod
102 | def response_code_handler(cls, response) -> Union[Dict, None]:
103 | """
104 | 处理接口响应,默认断言数据为code码,如果接口没有code码,则返回None
105 | @param response:
106 | @return:
107 | """
108 | try:
109 | data = cls.data_handle(response)
110 | return {"code": {"jsonpath": "$.code", "type": "==",
111 | "value": data['code'], "AssertType": None}}
112 | except KeyError:
113 | return None
114 | except NameError:
115 | return None
116 |
117 | @classmethod
118 | def request_type_handler(cls, method: Text) -> Text:
119 | """ 处理请求类型,有params、json、file,需要根据公司的业务情况自己调整 """
120 | if method == 'GET':
121 | # 如我们公司只有get请求是prams,其他都是json的
122 | return 'params'
123 | return 'json'
124 |
125 | @classmethod
126 | def data_handle(cls, dict_str) -> Any:
127 | """处理接口请求、响应的数据,如null、true格式问题"""
128 | try:
129 | if dict_str != "":
130 | if 'null' in dict_str:
131 | dict_str = dict_str.replace('null', 'None')
132 | if 'true' in dict_str:
133 | dict_str = dict_str.replace('true', 'True')
134 | if 'false' in dict_str:
135 | dict_str = dict_str.replace('false', 'False')
136 | dict_str = ast.literal_eval(dict_str)
137 | if dict_str == "":
138 | dict_str = None
139 | return dict_str
140 | except Exception as exc:
141 | raise exc
142 |
143 | @classmethod
144 | def token_handle(cls, header) -> Dict:
145 | """
146 | 提取请求头参数
147 | :param header:
148 | :return:
149 | """
150 | # 这里是将所有请求头的数据,全部都拦截出来了
151 | # 如果公司只需要部分参数,可以在这里加判断过滤
152 | headers = {}
153 | for key, value in header.items():
154 | headers[key] = value
155 | return headers
156 |
157 | def host_handle(self, url: Text) -> Tuple:
158 | """
159 | 解析 url
160 | :param url: https://xxxx.test.xxxx.com/#/goods/listShop
161 | :return: https://xxxx.test.xxxx.com/
162 | """
163 | host = None
164 | # 循环遍历需要过滤的hosts数据
165 | for i in self.url:
166 | # 这里主要是判断,如果我们conf.py中有配置这个域名,则用例中展示 ”${{host}}“,动态获取用例host
167 | # 大家可以在这里改成自己公司的host地址
168 | if 'https://www.wanandroid.com' in url:
169 | host = '${{host}}'
170 | elif i in url:
171 | host = i
172 | return host
173 |
174 | def url_path_handle(self, url: Text):
175 | """
176 | 解析 url_path
177 | :param url: https://xxxx.test.xxxx.com/shopList/json
178 | :return: /shopList/json
179 | """
180 | url_path = None
181 | # 循环需要拦截的域名
182 | for path in self.url:
183 | if path in url:
184 | url_path = url.split(path)[-1]
185 | return url_path
186 |
187 | def yaml_cases(self, data: Dict) -> None:
188 | """
189 | 写入 yaml 数据
190 | :param data: 测试用例数据
191 | :return:
192 | """
193 | with open(self.file, "a", encoding="utf-8") as file:
194 | yaml.dump(data, file, Dumper=yaml.RoundTripDumper, allow_unicode=True)
195 | file.write('\n')
196 |
197 | def get_url_handler(self, url: Text) -> Tuple:
198 | """
199 | 将 url 中的参数 转换成字典
200 | :param url: /trade?tradeNo=&outTradeId=11
201 | :return: {“outTradeId”: 11}
202 | """
203 | result = None
204 | url_path = None
205 | for i in self.url:
206 | if i in url:
207 | query = urlparse(url).query
208 | # 将字符串转换为字典
209 | params = parse_qs(query)
210 | # 所得的字典的value都是以列表的形式存在,如请求url中的参数值为空,则字典中不会有该参数
211 | result = {key: params[key][0] for key in params}
212 | url = url[0:url.rfind('?')]
213 | url_path = url.split(i)[-1]
214 | return result, url_path
215 |
216 |
217 | # 1、本机需要设置代理,默认端口为: 8080
218 | # 2、控制台输入 mitmweb -s .\utils\recording\mitmproxy_control.py - p 8888命令开启代理模式进行录制
219 |
220 |
221 | addons = [
222 | Counter(["https://www.wanandroid.com"])
223 | ]
224 |
--------------------------------------------------------------------------------
/utils/requests_tool/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/requests_tool/__init__.py
--------------------------------------------------------------------------------
/utils/requests_tool/dependent_case.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | """
4 | import ast
5 | import json
6 | from typing import Text, Dict, Union, List
7 | from jsonpath import jsonpath
8 | from utils.requests_tool.request_control import RequestControl
9 | from utils.mysql_tool.mysql_control import SetUpMySQL
10 | from utils.read_files_tools.regular_control import regular, cache_regular
11 | from utils.other_tools.jsonpath_date_replace import jsonpath_replace
12 | from utils.logging_tool.log_control import WARNING
13 | from utils.other_tools.models import DependentType
14 | from utils.other_tools.models import TestCase, DependentCaseData, DependentData
15 | from utils.other_tools.exceptions import ValueNotFoundError
16 | from utils.cache_process.cache_control import CacheHandler
17 | from utils import config
18 |
19 |
20 | class DependentCase:
21 | """ 处理依赖相关的业务 """
22 |
23 | def __init__(self, dependent_yaml_case: TestCase):
24 | self.__yaml_case = dependent_yaml_case
25 |
26 | @classmethod
27 | def get_cache(cls, case_id: Text) -> Dict:
28 | """
29 | 获取缓存用例池中的数据,通过 case_id 提取
30 | :param case_id:
31 | :return: case_id_01
32 | """
33 | _case_data = CacheHandler.get_cache(case_id)
34 | return _case_data
35 |
36 | @classmethod
37 | def jsonpath_data(
38 | cls,
39 | obj: Dict,
40 | expr: Text) -> list:
41 | """
42 | 通过jsonpath提取依赖的数据
43 | :param obj: 对象信息
44 | :param expr: jsonpath 方法
45 | :return: 提取到的内容值,返回是个数组
46 |
47 | 对象: {"data": applyID} --> jsonpath提取方法: $.data.data.[0].applyId
48 | """
49 |
50 | _jsonpath_data = jsonpath(obj, expr)
51 | # 判断是否正常提取到数据,如未提取到,则抛异常
52 | if _jsonpath_data is False:
53 | raise ValueNotFoundError(
54 | f"jsonpath提取失败!\n 提取的数据: {obj} \n jsonpath规则: {expr}"
55 | )
56 | return _jsonpath_data
57 |
58 | @classmethod
59 | def set_cache_value(cls, dependent_data: "DependentData") -> Union[Text, None]:
60 | """
61 | 获取依赖中是否需要将数据存入缓存中
62 | """
63 | try:
64 | return dependent_data.set_cache
65 | except KeyError:
66 | return None
67 |
68 | @classmethod
69 | def replace_key(cls, dependent_data: "DependentData"):
70 | """ 获取需要替换的内容 """
71 | try:
72 | _replace_key = dependent_data.replace_key
73 | return _replace_key
74 | except KeyError:
75 | return None
76 |
77 | def url_replace(
78 | self,
79 | replace_key: Text,
80 | jsonpath_dates: Dict,
81 | jsonpath_data: list) -> None:
82 | """
83 | url中的动态参数替换
84 | # 如: 一般有些接口的参数在url中,并且没有参数名称, /api/v1/work/spu/approval/spuApplyDetails/{id}
85 | # 那么可以使用如下方式编写用例, 可以使用 $url_params{}替换,
86 | # 如/api/v1/work/spu/approval/spuApplyDetails/$url_params{id}
87 | :param jsonpath_data: jsonpath 解析出来的数据值
88 | :param replace_key: 用例中需要替换数据的 replace_key
89 | :param jsonpath_dates: jsonpath 存放的数据值
90 | :return:
91 | """
92 |
93 | if "$url_param" in replace_key:
94 | _url = self.__yaml_case.url.replace(replace_key, str(jsonpath_data[0]))
95 | jsonpath_dates['$.url'] = _url
96 | else:
97 | jsonpath_dates[replace_key] = jsonpath_data[0]
98 |
99 | def _dependent_type_for_sql(
100 | self,
101 | setup_sql: List,
102 | dependence_case_data: "DependentCaseData",
103 | jsonpath_dates: Dict) -> None:
104 | """
105 | 判断依赖类型为 sql,程序中的依赖参数从 数据库中提取数据
106 | @param setup_sql: 前置sql语句
107 | @param dependence_case_data: 依赖的数据
108 | @param jsonpath_dates: 依赖相关的用例数据
109 | @return:
110 | """
111 | # 判断依赖数据类型,依赖 sql中的数据
112 | if setup_sql is not None:
113 | if config.mysql_db.switch:
114 | setup_sql = ast.literal_eval(cache_regular(str(setup_sql)))
115 | sql_data = SetUpMySQL().setup_sql_data(sql=setup_sql)
116 | dependent_data = dependence_case_data.dependent_data
117 | for i in dependent_data:
118 | _jsonpath = i.jsonpath
119 | jsonpath_data = self.jsonpath_data(obj=sql_data, expr=_jsonpath)
120 | _set_value = self.set_cache_value(i)
121 | _replace_key = self.replace_key(i)
122 | if _set_value is not None:
123 | CacheHandler.update_cache(cache_name=_set_value, value=jsonpath_data[0])
124 | # Cache(_set_value).set_caches(jsonpath_data[0])
125 | if _replace_key is not None:
126 | jsonpath_dates[_replace_key] = jsonpath_data[0]
127 | self.url_replace(
128 | replace_key=_replace_key,
129 | jsonpath_dates=jsonpath_dates,
130 | jsonpath_data=jsonpath_data,
131 | )
132 | else:
133 | WARNING.logger.warning("检查到数据库开关为关闭状态,请确认配置")
134 |
135 | def dependent_handler(
136 | self,
137 | _jsonpath: Text,
138 | set_value: Text,
139 | replace_key: Text,
140 | jsonpath_dates: Dict,
141 | data: Dict,
142 | dependent_type: int
143 | ) -> None:
144 | """ 处理数据替换 """
145 | jsonpath_data = self.jsonpath_data(
146 | data,
147 | _jsonpath
148 | )
149 | if set_value is not None:
150 | if len(jsonpath_data) > 1:
151 | CacheHandler.update_cache(cache_name=set_value, value=jsonpath_data)
152 | else:
153 | CacheHandler.update_cache(cache_name=set_value, value=jsonpath_data[0])
154 | if replace_key is not None:
155 | if dependent_type == 0:
156 | jsonpath_dates[replace_key] = jsonpath_data[0]
157 | self.url_replace(replace_key=replace_key, jsonpath_dates=jsonpath_dates,
158 | jsonpath_data=jsonpath_data)
159 |
160 | def is_dependent(self) -> Union[Dict, bool]:
161 | """
162 | 判断是否有数据依赖
163 | :return:
164 | """
165 |
166 | # 获取用例中的dependent_type值,判断该用例是否需要执行依赖
167 | _dependent_type = self.__yaml_case.dependence_case
168 | # 获取依赖用例数据
169 | _dependence_case_dates = self.__yaml_case.dependence_case_data
170 | _setup_sql = self.__yaml_case.setup_sql
171 | # 判断是否有依赖
172 | if _dependent_type is True:
173 | # 读取依赖相关的用例数据
174 | jsonpath_dates = {}
175 | # 循环所有需要依赖的数据
176 | try:
177 | for dependence_case_data in _dependence_case_dates:
178 | _case_id = dependence_case_data.case_id
179 | # 判断依赖数据为sql,case_id需要写成self,否则程序中无法获取case_id
180 | if _case_id == 'self':
181 | self._dependent_type_for_sql(
182 | setup_sql=_setup_sql,
183 | dependence_case_data=dependence_case_data,
184 | jsonpath_dates=jsonpath_dates)
185 | else:
186 | re_data = regular(str(self.get_cache(_case_id)))
187 | re_data = ast.literal_eval(cache_regular(str(re_data)))
188 | res = RequestControl(re_data).http_request()
189 | if dependence_case_data.dependent_data is not None:
190 | dependent_data = dependence_case_data.dependent_data
191 | for i in dependent_data:
192 |
193 | _case_id = dependence_case_data.case_id
194 | _jsonpath = i.jsonpath
195 | _request_data = self.__yaml_case.data
196 | _replace_key = self.replace_key(i)
197 | _set_value = self.set_cache_value(i)
198 | # 判断依赖数据类型, 依赖 response 中的数据
199 | if i.dependent_type == DependentType.RESPONSE.value:
200 | self.dependent_handler(
201 | data=json.loads(res.response_data),
202 | _jsonpath=_jsonpath,
203 | set_value=_set_value,
204 | replace_key=_replace_key,
205 | jsonpath_dates=jsonpath_dates,
206 | dependent_type=0
207 | )
208 |
209 | # 判断依赖数据类型, 依赖 request 中的数据
210 | elif i.dependent_type == DependentType.REQUEST.value:
211 | self.dependent_handler(
212 | data=res.body,
213 | _jsonpath=_jsonpath,
214 | set_value=_set_value,
215 | replace_key=_replace_key,
216 | jsonpath_dates=jsonpath_dates,
217 | dependent_type=1
218 | )
219 |
220 | else:
221 | raise ValueError(
222 | "依赖的dependent_type不正确,只支持request、response、sql依赖\n"
223 | f"当前填写内容: {i.dependent_type}"
224 | )
225 | return jsonpath_dates
226 | except KeyError as exc:
227 | # pass
228 | raise ValueNotFoundError(
229 | f"dependence_case_data依赖用例中,未找到 {exc} 参数,请检查是否填写"
230 | f"如已填写,请检查是否存在yaml缩进问题"
231 | ) from exc
232 | except TypeError as exc:
233 | raise ValueNotFoundError(
234 | "dependence_case_data下的所有内容均不能为空!"
235 | "请检查相关数据是否填写,如已填写,请检查缩进问题"
236 | ) from exc
237 | else:
238 | return False
239 |
240 | def get_dependent_data(self) -> None:
241 | """
242 | jsonpath 和 依赖的数据,进行替换
243 | :return:
244 | """
245 | _dependent_data = DependentCase(self.__yaml_case).is_dependent()
246 | _new_data = None
247 | # 判断有依赖
248 | if _dependent_data is not None and _dependent_data is not False:
249 | # if _dependent_data is not False:
250 | for key, value in _dependent_data.items():
251 | # 通过jsonpath判断出需要替换数据的位置
252 | _change_data = key.split(".")
253 | # jsonpath 数据解析
254 | # 不要删 这个yaml_case
255 | yaml_case = self.__yaml_case
256 | _new_data = jsonpath_replace(change_data=_change_data, key_name='yaml_case')
257 | # 最终提取到的数据,转换成 __yaml_case.data
258 | _new_data += ' = ' + str(value)
259 | exec(_new_data)
260 |
--------------------------------------------------------------------------------
/utils/requests_tool/encryption_algorithm_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File : encryption_algorithm_control
5 | # @describe:
6 | """
7 |
8 | import hashlib
9 | from hashlib import sha256
10 | import hmac
11 | from typing import Text
12 | import binascii
13 | from pyDes import des, ECB, PAD_PKCS5
14 |
15 |
16 | def hmac_sha256_encrypt(key, data):
17 | """hmac sha 256算法"""
18 | _key = key.encode('utf8')
19 | _data = data.encode('utf8')
20 | encrypt_data = hmac.new(_key, _data, digestmod=sha256).hexdigest()
21 | return encrypt_data
22 |
23 |
24 | def md5_encryption(value):
25 | """ md5 加密"""
26 | str_md5 = hashlib.md5(str(value).encode(encoding='utf-8')).hexdigest()
27 | return str_md5
28 |
29 |
30 | def sha1_secret_str(_str: Text):
31 | """
32 | 使用sha1加密算法,返回str加密后的字符串
33 | """
34 | encrypts = hashlib.sha1(_str.encode('utf-8')).hexdigest()
35 | return encrypts
36 |
37 |
38 | def des_encrypt(_str):
39 | """
40 | DES 加密
41 | :return: 加密后字符串,16进制
42 | """
43 | # 密钥,自行修改
44 | _key = 'PASSWORD'
45 | secret_key = _key
46 | _iv = secret_key
47 | key = des(secret_key, ECB, _iv, pad=None, padmode=PAD_PKCS5)
48 | _encrypt = key.encrypt(_str, padmode=PAD_PKCS5)
49 | return binascii.b2a_hex(_encrypt)
50 |
51 |
52 | def encryption(ency_type):
53 | """
54 | :param ency_type: 加密类型
55 | :return:
56 | """
57 |
58 | def decorator(func):
59 | def swapper(*args, **kwargs):
60 | res = func(*args, **kwargs)
61 | _data = res['body']
62 | if ency_type == "md5":
63 | def ency_value(data):
64 | if data is not None:
65 | for key, value in data.items():
66 | if isinstance(value, dict):
67 | ency_value(data=value)
68 | else:
69 | data[key] = md5_encryption(value)
70 | else:
71 | raise ValueError("暂不支持该加密规则,如有需要,请联系管理员")
72 | ency_value(_data)
73 | return res
74 |
75 | return swapper
76 |
77 | return decorator
78 |
--------------------------------------------------------------------------------
/utils/requests_tool/request_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 |
5 | """
6 | import ast
7 | import os
8 | import random
9 | import time
10 | import urllib
11 | from typing import Tuple, Dict, Union, Text
12 | import requests
13 | import urllib3
14 | from requests_toolbelt import MultipartEncoder
15 | from common.setting import ensure_path_sep
16 | from utils.other_tools.models import RequestType
17 | from utils.logging_tool.log_decorator import log_decorator
18 | from utils.mysql_tool.mysql_control import AssertExecution
19 | from utils.logging_tool.run_time_decorator import execution_duration
20 | from utils.other_tools.allure_data.allure_tools import allure_step, allure_step_no, allure_attach
21 | from utils.read_files_tools.regular_control import cache_regular
22 | from utils.requests_tool.set_current_request_cache import SetCurrentRequestCache
23 | from utils.other_tools.models import TestCase, ResponseData
24 | from utils import config
25 | # from utils.requests_tool.encryption_algorithm_control import encryption
26 |
27 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
28 |
29 |
30 | class RequestControl:
31 | """ 封装请求 """
32 |
33 | def __init__(self, yaml_case):
34 | self.__yaml_case = TestCase(**yaml_case)
35 |
36 | def file_data_exit(
37 | self,
38 | file_data) -> None:
39 | """判断上传文件时,data参数是否存在"""
40 | # 兼容又要上传文件,又要上传其他类型参数
41 | try:
42 | _data = self.__yaml_case.data
43 | for key, value in ast.literal_eval(cache_regular(str(_data)))['data'].items():
44 | file_data[key] = value
45 | except KeyError:
46 | ...
47 |
48 | @classmethod
49 | def multipart_data(
50 | cls,
51 | file_data: Dict):
52 | """ 处理上传文件数据 """
53 | multipart = MultipartEncoder(
54 | fields=file_data, # 字典格式
55 | boundary='-----------------------------' + str(random.randint(int(1e28), int(1e29 - 1)))
56 | )
57 | return multipart
58 |
59 | @classmethod
60 | def check_headers_str_null(
61 | cls,
62 | headers: Dict) -> Dict:
63 | """
64 | 兼容用户未填写headers或者header值为int
65 | @return:
66 | """
67 | headers = ast.literal_eval(cache_regular(str(headers)))
68 | if headers is None:
69 | headers = {"headers": None}
70 | else:
71 | for key, value in headers.items():
72 | if not isinstance(value, str):
73 | headers[key] = str(value)
74 | return headers
75 |
76 | @classmethod
77 | def multipart_in_headers(
78 | cls,
79 | request_data: Dict,
80 | header: Dict):
81 | """ 判断处理header为 Content-Type: multipart/form-data"""
82 | header = ast.literal_eval(cache_regular(str(header)))
83 | request_data = ast.literal_eval(cache_regular(str(request_data)))
84 |
85 | if header is None:
86 | header = {"headers": None}
87 | else:
88 | # 将header中的int转换成str
89 | for key, value in header.items():
90 | if not isinstance(value, str):
91 | header[key] = str(value)
92 | if "multipart/form-data" in str(header.values()):
93 | # 判断请求参数不为空, 并且参数是字典类型
94 | if request_data and isinstance(request_data, dict):
95 | # 当 Content-Type 为 "multipart/form-data"时,需要将数据类型转换成 str
96 | for key, value in request_data.items():
97 | if not isinstance(value, str):
98 | request_data[key] = str(value)
99 |
100 | request_data = MultipartEncoder(request_data)
101 | header['Content-Type'] = request_data.content_type
102 |
103 | return request_data, header
104 |
105 | def file_prams_exit(self) -> Dict:
106 | """判断上传文件接口,文件参数是否存在"""
107 | try:
108 | params = self.__yaml_case.data['params']
109 | except KeyError:
110 | params = None
111 | return params
112 |
113 | @classmethod
114 | def text_encode(
115 | cls,
116 | text: Text) -> Text:
117 | """unicode 解码"""
118 | return text.encode("utf-8").decode("utf-8")
119 |
120 | @classmethod
121 | def response_elapsed_total_seconds(
122 | cls,
123 | res) -> float:
124 | """获取接口响应时长"""
125 | try:
126 | return round(res.elapsed.total_seconds() * 1000, 2)
127 | except AttributeError:
128 | return 0.00
129 |
130 | def upload_file(
131 | self) -> Tuple:
132 | """
133 | 判断处理上传文件
134 | :return:
135 | """
136 | # 处理上传多个文件的情况
137 | _files = []
138 | file_data = {}
139 | # 兼容又要上传文件,又要上传其他类型参数
140 | self.file_data_exit(file_data)
141 | _data = self.__yaml_case.data
142 | for key, value in ast.literal_eval(cache_regular(str(_data)))['file'].items():
143 | file_path = ensure_path_sep("\\Files\\" + value)
144 | file_data[key] = (value, open(file_path, 'rb'), 'application/octet-stream')
145 | _files.append(file_data)
146 | # allure中展示该附件
147 | allure_attach(source=file_path, name=value, extension=value)
148 | multipart = self.multipart_data(file_data)
149 | # ast.literal_eval(cache_regular(str(_headers)))['Content-Type'] = multipart.content_type
150 | self.__yaml_case.headers['Content-Type'] = multipart.content_type
151 | params_data = ast.literal_eval(cache_regular(str(self.file_prams_exit())))
152 | return multipart, params_data, self.__yaml_case
153 |
154 | def request_type_for_json(
155 | self,
156 | headers: Dict,
157 | method: Text,
158 | **kwargs):
159 | """ 判断请求类型为json格式 """
160 | _headers = self.check_headers_str_null(headers)
161 | _data = self.__yaml_case.data
162 | _url = self.__yaml_case.url
163 | res = requests.request(
164 | method=method,
165 | url=cache_regular(str(_url)),
166 | json=ast.literal_eval(cache_regular(str(_data))),
167 | data={},
168 | headers=_headers,
169 | verify=False,
170 | params=None,
171 | **kwargs
172 | )
173 | return res
174 |
175 | def request_type_for_none(
176 | self,
177 | headers: Dict,
178 | method: Text,
179 | **kwargs) -> object:
180 | """判断 requestType 为 None"""
181 | _headers = self.check_headers_str_null(headers)
182 | _url = self.__yaml_case.url
183 | res = requests.request(
184 | method=method,
185 | url=cache_regular(_url),
186 | data=None,
187 | headers=_headers,
188 | verify=False,
189 | params=None,
190 | **kwargs
191 | )
192 | return res
193 |
194 | def request_type_for_params(
195 | self,
196 | headers: Dict,
197 | method: Text,
198 | **kwargs):
199 |
200 | """处理 requestType 为 params """
201 | _data = self.__yaml_case.data
202 | url = self.__yaml_case.url
203 | if _data is not None:
204 | # url 拼接的方式传参
205 | params_data = "?"
206 | for key, value in _data.items():
207 | if value is None or value == '':
208 | params_data += (key + "&")
209 | else:
210 | params_data += (key + "=" + str(value) + "&")
211 | url = self.__yaml_case.url + params_data[:-1]
212 | _headers = self.check_headers_str_null(headers)
213 | res = requests.request(
214 | method=method,
215 | url=cache_regular(url),
216 | headers=_headers,
217 | verify=False,
218 | data={},
219 | params=None,
220 | **kwargs)
221 | return res
222 |
223 | def request_type_for_file(
224 | self,
225 | method: Text,
226 | headers,
227 | **kwargs):
228 | """处理 requestType 为 file 类型"""
229 | multipart = self.upload_file()
230 | yaml_data = multipart[2]
231 | _headers = multipart[2].headers
232 | _headers = self.check_headers_str_null(_headers)
233 | res = requests.request(
234 | method=method,
235 | url=cache_regular(yaml_data.url),
236 | data=multipart[0],
237 | params=multipart[1],
238 | headers=ast.literal_eval(cache_regular(str(_headers))),
239 | verify=False,
240 | **kwargs
241 | )
242 | return res
243 |
244 | def request_type_for_data(
245 | self,
246 | headers: Dict,
247 | method: Text,
248 | **kwargs):
249 | """判断 requestType 为 data 类型"""
250 | data = self.__yaml_case.data
251 | _data, _headers = self.multipart_in_headers(
252 | ast.literal_eval(cache_regular(str(data))),
253 | headers
254 | )
255 | _url = self.__yaml_case.url
256 | res = requests.request(
257 | method=method,
258 | url=cache_regular(_url),
259 | data=_data,
260 | headers=_headers,
261 | verify=False,
262 | **kwargs)
263 |
264 | return res
265 |
266 | @classmethod
267 | def get_export_api_filename(cls, res):
268 | """ 处理导出文件 """
269 | content_disposition = res.headers.get('content-disposition')
270 | filename_code = content_disposition.split("=")[-1] # 分隔字符串,提取文件名
271 | filename = urllib.parse.unquote(filename_code) # url解码
272 | return filename
273 |
274 | def request_type_for_export(
275 | self,
276 | headers: Dict,
277 | method: Text,
278 | **kwargs):
279 | """判断 requestType 为 export 导出类型"""
280 | _headers = self.check_headers_str_null(headers)
281 | _data = self.__yaml_case.data
282 | _url = self.__yaml_case.url
283 | res = requests.request(
284 | method=method,
285 | url=cache_regular(_url),
286 | json=ast.literal_eval(cache_regular(str(_data))),
287 | headers=_headers,
288 | verify=False,
289 | stream=False,
290 | data={},
291 | **kwargs)
292 | filepath = os.path.join(ensure_path_sep("\\Files\\"), self.get_export_api_filename(res)) # 拼接路径
293 | if res.status_code == 200:
294 | if res.text: # 判断文件内容是否为空
295 | with open(filepath, 'wb') as file:
296 | # iter_content循环读取信息写入,chunk_size设置文件大小
297 | for chunk in res.iter_content(chunk_size=1):
298 | file.write(chunk)
299 | else:
300 | print("文件为空")
301 |
302 | return res
303 |
304 | @classmethod
305 | def _request_body_handler(cls, data: Dict, request_type: Text) -> Union[None, Dict]:
306 | """处理请求参数 """
307 | if request_type.upper() == 'PARAMS':
308 | return None
309 | else:
310 | return data
311 |
312 | @classmethod
313 | def _sql_data_handler(cls, sql_data, res):
314 | """处理 sql 参数 """
315 | # 判断数据库开关,开启状态,则返回对应的数据
316 | if config.mysql_db.switch and sql_data is not None:
317 | sql_data = AssertExecution().assert_execution(
318 | sql=sql_data,
319 | resp=res.json()
320 | )
321 |
322 | else:
323 | sql_data = {"sql": None}
324 | return sql_data
325 |
326 | def _check_params(
327 | self,
328 | res,
329 | yaml_data: "TestCase",
330 | ) -> "ResponseData":
331 | data = ast.literal_eval(cache_regular(str(yaml_data.data)))
332 | _data = {
333 | "url": res.url,
334 | "is_run": yaml_data.is_run,
335 | "detail": yaml_data.detail,
336 | "response_data": res.text,
337 | # 这个用于日志专用,判断如果是get请求,直接打印url
338 | "request_body": self._request_body_handler(
339 | data, yaml_data.requestType
340 | ),
341 | "method": res.request.method,
342 | "sql_data": self._sql_data_handler(sql_data=ast.literal_eval(cache_regular(str(yaml_data.sql))), res=res),
343 | "yaml_data": yaml_data,
344 | "headers": res.request.headers,
345 | "cookie": res.cookies,
346 | "assert_data": yaml_data.assert_data,
347 | "res_time": self.response_elapsed_total_seconds(res),
348 | "status_code": res.status_code,
349 | "teardown": yaml_data.teardown,
350 | "teardown_sql": yaml_data.teardown_sql,
351 | "body": data
352 | }
353 | # 抽离出通用模块,判断 http_request 方法中的一些数据校验
354 | return ResponseData(**_data)
355 |
356 | @classmethod
357 | def api_allure_step(
358 | cls,
359 | *,
360 | url: Text,
361 | headers: Text,
362 | method: Text,
363 | data: Text,
364 | assert_data: Text,
365 | res_time: Text,
366 | res: Text
367 | ) -> None:
368 | """ 在allure中记录请求数据 """
369 | allure_step_no(f"请求URL: {url}")
370 | allure_step_no(f"请求方式: {method}")
371 | allure_step("请求头: ", headers)
372 | allure_step("请求数据: ", data)
373 | allure_step("预期数据: ", assert_data)
374 | _res_time = res_time
375 | allure_step_no(f"响应耗时(ms): {str(_res_time)}")
376 | allure_step("响应结果: ", res)
377 |
378 | @log_decorator(True)
379 | @execution_duration(3000)
380 | # @encryption("md5")
381 | def http_request(
382 | self,
383 | dependent_switch=True,
384 | **kwargs
385 | ):
386 | """
387 | 请求封装
388 | :param dependent_switch:
389 | :param kwargs:
390 | :return:
391 | """
392 | from utils.requests_tool.dependent_case import DependentCase
393 | requests_type_mapping = {
394 | RequestType.JSON.value: self.request_type_for_json,
395 | RequestType.NONE.value: self.request_type_for_none,
396 | RequestType.PARAMS.value: self.request_type_for_params,
397 | RequestType.FILE.value: self.request_type_for_file,
398 | RequestType.DATA.value: self.request_type_for_data,
399 | RequestType.EXPORT.value: self.request_type_for_export
400 | }
401 |
402 | is_run = ast.literal_eval(cache_regular(str(self.__yaml_case.is_run)))
403 | # 判断用例是否执行
404 | if is_run is True or is_run is None:
405 | # 处理多业务逻辑
406 | if dependent_switch is True:
407 | DependentCase(self.__yaml_case).get_dependent_data()
408 |
409 | res = requests_type_mapping.get(self.__yaml_case.requestType)(
410 | headers=self.__yaml_case.headers,
411 | method=self.__yaml_case.method,
412 | **kwargs
413 | )
414 |
415 | if self.__yaml_case.sleep is not None:
416 | time.sleep(self.__yaml_case.sleep)
417 |
418 | _res_data = self._check_params(
419 | res=res,
420 | yaml_data=self.__yaml_case)
421 |
422 | self.api_allure_step(
423 | url=_res_data.url,
424 | headers=str(_res_data.headers),
425 | method=_res_data.method,
426 | data=str(_res_data.body),
427 | assert_data=str(_res_data.assert_data),
428 | res_time=str(_res_data.res_time),
429 | res=_res_data.response_data
430 | )
431 | # 将当前请求数据存入缓存中
432 | SetCurrentRequestCache(
433 | current_request_set_cache=self.__yaml_case.current_request_set_cache,
434 | request_data=self.__yaml_case.data,
435 | response_data=res
436 | ).set_caches_main()
437 |
438 | return _res_data
439 |
440 |
--------------------------------------------------------------------------------
/utils/requests_tool/set_current_request_cache.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File : set_current_request_cache
5 | # @describe:
6 | """
7 | import json
8 | from typing import Text
9 | from jsonpath import jsonpath
10 | from utils.other_tools.exceptions import ValueNotFoundError
11 | from utils.cache_process.cache_control import CacheHandler
12 |
13 |
14 | class SetCurrentRequestCache:
15 | """将用例中的请求或者响应内容存入缓存"""
16 |
17 | def __init__(
18 | self,
19 | current_request_set_cache,
20 | request_data,
21 | response_data
22 | ):
23 | self.current_request_set_cache = current_request_set_cache
24 | self.request_data = {"data": request_data}
25 | self.response_data = response_data.text
26 |
27 | def set_request_cache(
28 | self,
29 | jsonpath_value: Text,
30 | cache_name: Text) -> None:
31 | """将接口的请求参数存入缓存"""
32 | _request_data = jsonpath(
33 | self.request_data,
34 | jsonpath_value
35 | )
36 | if _request_data is not False:
37 | CacheHandler.update_cache(cache_name=cache_name, value=_request_data[0])
38 | # Cache(cache_name).set_caches(_request_data[0])
39 | else:
40 | raise ValueNotFoundError(
41 | "缓存设置失败,程序中未检测到需要缓存的数据。"
42 | f"请求参数: {self.request_data}"
43 | f"提取的 jsonpath 内容: {jsonpath_value}"
44 | )
45 |
46 | def set_response_cache(
47 | self,
48 | jsonpath_value: Text,
49 | cache_name
50 | ):
51 | """将响应结果存入缓存"""
52 | _response_data = jsonpath(json.loads(self.response_data), jsonpath_value)
53 | if _response_data is not False:
54 | CacheHandler.update_cache(cache_name=cache_name, value=_response_data[0])
55 | # Cache(cache_name).set_caches(_response_data[0])
56 | else:
57 | raise ValueNotFoundError("缓存设置失败,程序中未检测到需要缓存的数据。"
58 | f"请求参数: {self.response_data}"
59 | f"提取的 jsonpath 内容: {jsonpath_value}")
60 |
61 | def set_caches_main(self):
62 | """设置缓存"""
63 | if self.current_request_set_cache is not None:
64 | for i in self.current_request_set_cache:
65 | _jsonpath = i.jsonpath
66 | _cache_name = i.name
67 | if i.type == 'request':
68 | self.set_request_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)
69 | elif i.type == 'response':
70 | self.set_response_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)
71 |
--------------------------------------------------------------------------------
/utils/requests_tool/teardown_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | # @File : teardownControl
5 | # @describe: 请求后置处理
6 | """
7 | import ast
8 | import json
9 | from typing import Dict, Text
10 | from jsonpath import jsonpath
11 | from utils.requests_tool.request_control import RequestControl
12 | from utils.read_files_tools.regular_control import cache_regular, sql_regular, regular
13 | from utils.other_tools.jsonpath_date_replace import jsonpath_replace
14 | from utils.mysql_tool.mysql_control import MysqlDB
15 | from utils.logging_tool.log_control import WARNING
16 | from utils.other_tools.models import ResponseData, TearDown, SendRequest, ParamPrepare
17 | from utils.other_tools.exceptions import JsonpathExtractionFailed, ValueNotFoundError
18 | from utils.cache_process.cache_control import CacheHandler
19 | from utils import config
20 |
21 |
22 | class TearDownHandler:
23 | """ 处理yaml格式后置请求 """
24 | def __init__(self, res: "ResponseData"):
25 | self._res = res
26 |
27 | @classmethod
28 | def jsonpath_replace_data(
29 | cls,
30 | replace_key: Text,
31 | replace_value: Dict) -> Text:
32 |
33 | """ 通过jsonpath判断出需要替换数据的位置 """
34 | _change_data = replace_key.split(".")
35 | # jsonpath 数据解析
36 | _new_data = jsonpath_replace(
37 | change_data=_change_data,
38 | key_name='_teardown_case',
39 | data_switch=False
40 | )
41 |
42 | if not isinstance(replace_value, str):
43 | _new_data += f" = {replace_value}"
44 | # 最终提取到的数据,转换成 _teardown_case[xxx][xxx]
45 | else:
46 | _new_data += f" = '{replace_value}'"
47 | return _new_data
48 |
49 | @classmethod
50 | def get_cache_name(
51 | cls,
52 | replace_key: Text,
53 | resp_case_data: Dict) -> None:
54 | """
55 | 获取缓存名称,并且讲提取到的数据写入缓存
56 | """
57 | if "$set_cache{" in replace_key and "}" in replace_key:
58 | start_index = replace_key.index("$set_cache{")
59 | end_index = replace_key.index("}", start_index)
60 | old_value = replace_key[start_index:end_index + 2]
61 | cache_name = old_value[11:old_value.index("}")]
62 | CacheHandler.update_cache(cache_name=cache_name, value=resp_case_data)
63 | # Cache(cache_name).set_caches(resp_case_data)
64 |
65 | @classmethod
66 | def regular_testcase(cls, teardown_case: Dict) -> Dict:
67 | """处理测试用例中的动态数据"""
68 | test_case = regular(str(teardown_case))
69 | test_case = ast.literal_eval(cache_regular(str(test_case)))
70 | return test_case
71 |
72 | @classmethod
73 | def teardown_http_requests(cls, teardown_case: Dict) -> "ResponseData":
74 | """
75 | 发送后置请求
76 | @param teardown_case: 后置用例
77 | @return:
78 | """
79 |
80 | test_case = cls.regular_testcase(teardown_case)
81 | res = RequestControl(test_case).http_request(
82 | dependent_switch=False
83 | )
84 | return res
85 |
86 | def dependent_type_response(
87 | self,
88 | teardown_case_data: "SendRequest",
89 | resp_data: Dict) -> Text:
90 | """
91 | 判断依赖类型为当前执行用例响应内容
92 | :param : teardown_case_data: teardown中的用例内容
93 | :param : resp_data: 需要替换的内容
94 | :return:
95 | """
96 | _replace_key = teardown_case_data.replace_key
97 | _response_dependent = jsonpath(
98 | obj=resp_data,
99 | expr=teardown_case_data.jsonpath
100 | )
101 | # 如果提取到数据,则进行下一步
102 | if _response_dependent is not False:
103 | _resp_case_data = _response_dependent[0]
104 | data = self.jsonpath_replace_data(
105 | replace_key=_replace_key,
106 | replace_value=_resp_case_data
107 | )
108 | else:
109 | raise JsonpathExtractionFailed(
110 | f"jsonpath提取失败,替换内容: {resp_data} \n"
111 | f"jsonpath: {teardown_case_data.jsonpath}"
112 | )
113 | return data
114 |
115 | def dependent_type_request(
116 | self,
117 | teardown_case_data: Dict,
118 | request_data: Dict) -> None:
119 | """
120 | 判断依赖类型为请求内容
121 | :param : teardown_case_data: teardown中的用例内容
122 | :param : request_data: 需要替换的内容
123 | :return:
124 | """
125 | try:
126 | _request_set_value = teardown_case_data['set_value']
127 | _request_dependent = jsonpath(
128 | obj=request_data,
129 | expr=teardown_case_data['jsonpath']
130 | )
131 | if _request_dependent is not False:
132 | _request_case_data = _request_dependent[0]
133 | self.get_cache_name(
134 | replace_key=_request_set_value,
135 | resp_case_data=_request_case_data
136 | )
137 | else:
138 | raise JsonpathExtractionFailed(
139 | f"jsonpath提取失败,替换内容: {request_data} \n"
140 | f"jsonpath: {teardown_case_data['jsonpath']}"
141 | )
142 | except KeyError as exc:
143 | raise ValueNotFoundError("teardown中缺少set_value参数,请检查用例是否正确") from exc
144 |
145 | def dependent_self_response(
146 | self,
147 | teardown_case_data: "ParamPrepare",
148 | res: Dict,
149 | resp_data: Dict) -> None:
150 | """
151 | 判断依赖类型为依赖用例ID自己响应的内容
152 | :param : teardown_case_data: teardown中的用例内容
153 | :param : resp_data: 需要替换的内容
154 | :param : res: 接口响应的内容
155 | :return:
156 | """
157 | try:
158 | _set_value = teardown_case_data.set_cache
159 | _response_dependent = jsonpath(
160 | obj=res,
161 | expr=teardown_case_data.jsonpath
162 | )
163 | # 如果提取到数据,则进行下一步
164 | if _response_dependent is not False:
165 | _resp_case_data = _response_dependent[0]
166 | # 拿到 set_cache 然后将数据写入缓存
167 | # Cache(_set_value).set_caches(_resp_case_data)
168 | CacheHandler.update_cache(cache_name=_set_value, value=_resp_case_data)
169 | self.get_cache_name(
170 | replace_key=_set_value,
171 | resp_case_data=_resp_case_data
172 | )
173 | else:
174 | raise JsonpathExtractionFailed(
175 | f"jsonpath提取失败,替换内容: {resp_data} \n"
176 | f"jsonpath: {teardown_case_data.jsonpath}")
177 | except KeyError as exc:
178 | raise ValueNotFoundError("teardown中缺少set_cache参数,请检查用例是否正确") from exc
179 |
180 | @classmethod
181 | def dependent_type_cache(cls, teardown_case: "SendRequest") -> Text:
182 | """
183 | 判断依赖类型为从缓存中处理
184 | :param : teardown_case_data: teardown中的用例内容
185 | :return:
186 | """
187 | if teardown_case.dependent_type == 'cache':
188 | _cache_name = teardown_case.cache_data
189 | _replace_key = teardown_case.replace_key
190 | # 通过jsonpath判断出需要替换数据的位置
191 | _change_data = _replace_key.split(".")
192 | _new_data = jsonpath_replace(
193 | change_data=_change_data,
194 | key_name='_teardown_case',
195 | data_switch=False
196 | )
197 | # jsonpath 数据解析
198 | value_types = ['int:', 'bool:', 'list:', 'dict:', 'tuple:', 'float:']
199 | if any(i in _cache_name for i in value_types) is True:
200 | # _cache_data = Cache(_cache_name.split(':')[1]).get_cache()
201 | _cache_data = CacheHandler.get_cache(_cache_name.split(':')[1])
202 | _new_data += f" = {_cache_data}"
203 |
204 | # 最终提取到的数据,转换成 _teardown_case[xxx][xxx]
205 | else:
206 | # _cache_data = Cache(_cache_name).get_cache()
207 | _cache_data = CacheHandler.get_cache(_cache_name)
208 | _new_data += f" = '{_cache_data}'"
209 |
210 | return _new_data
211 |
212 | def send_request_handler(
213 | self, data: "TearDown",
214 | resp_data: Dict,
215 | request_data: Dict
216 | ) -> None:
217 | """
218 | 后置请求处理
219 | @return:
220 | """
221 | _send_request = data.send_request
222 | _case_id = data.case_id
223 | # _teardown_case = ast.literal_eval(Cache('case_process').get_cache())[_case_id]
224 | _teardown_case = CacheHandler.get_cache(_case_id)
225 | for i in _send_request:
226 | if i.dependent_type == 'cache':
227 | exec(self.dependent_type_cache(teardown_case=i))
228 | # 判断从响应内容提取数据
229 | if i.dependent_type == 'response':
230 | exec(
231 | self.dependent_type_response(
232 | teardown_case_data=i,
233 | resp_data=resp_data)
234 | )
235 | # 判断请求中的数据
236 | elif i.dependent_type == 'request':
237 | self.dependent_type_request(
238 | teardown_case_data=i,
239 | request_data=request_data
240 | )
241 |
242 | test_case = self.regular_testcase(_teardown_case)
243 | self.teardown_http_requests(test_case)
244 |
245 | def param_prepare_request_handler(
246 | self,
247 | data: "TearDown",
248 | resp_data: Dict) -> None:
249 | """
250 | 前置请求处理
251 | @param data:
252 | @param resp_data:
253 | @return:
254 | """
255 | _case_id = data.case_id
256 | # _teardown_case = ast.literal_eval(Cache('case_process').get_cache())[_case_id]
257 | _teardown_case = CacheHandler.get_cache(_case_id)
258 | _param_prepare = data.param_prepare
259 | res = self.teardown_http_requests(_teardown_case)
260 | for i in _param_prepare:
261 | # 判断请求类型为自己,拿到当前case_id自己的响应
262 | if i.dependent_type == 'self_response':
263 | self.dependent_self_response(
264 | teardown_case_data=i,
265 | resp_data=resp_data,
266 | res=json.loads(res.response_data)
267 | )
268 |
269 | def teardown_handle(self) -> None:
270 | """
271 | 为什么在这里需要单独区分 param_prepare 和 send_request
272 | 假设此时我们有用例A,teardown中我们需要执行用例B
273 |
274 | 那么考虑用户可能需要获取获取teardown的用例B的响应内容,也有可能需要获取用例A的响应内容,
275 | 因此我们这里需要通过关键词去做区分。这里需要考虑到,假设我们需要拿到B用例的响应,那么就需要先发送请求然后在拿到响应数据
276 |
277 | 那如果我们需要拿到A接口的响应,此时我们就不需要在额外发送请求了,因此我们需要区分一个是前置准备param_prepare,
278 | 一个是发送请求send_request
279 | @return:
280 | """
281 | # 拿到用例信息
282 | _teardown_data = self._res.teardown
283 | # 获取接口的响应内容
284 | _resp_data = self._res.response_data
285 | # 获取接口的请求参数
286 | _request_data = self._res.yaml_data.data
287 | # 判断如果没有 teardown
288 | if _teardown_data is not None:
289 | # 循环 teardown中的接口
290 | for _data in _teardown_data:
291 | if _data.param_prepare is not None:
292 | self.param_prepare_request_handler(
293 | data=_data,
294 | resp_data=json.loads(_resp_data)
295 | )
296 | elif _data.send_request is not None:
297 | self.send_request_handler(
298 | data=_data,
299 | request_data=_request_data,
300 | resp_data=json.loads(_resp_data)
301 | )
302 | self.teardown_sql()
303 |
304 | def teardown_sql(self) -> None:
305 | """处理后置sql"""
306 |
307 | sql_data = self._res.teardown_sql
308 | _response_data = self._res.response_data
309 | if sql_data is not None:
310 | for i in sql_data:
311 | if config.mysql_db.switch:
312 | _sql_data = sql_regular(value=i, res=json.loads(_response_data))
313 | MysqlDB().execute(cache_regular(_sql_data))
314 | else:
315 | WARNING.logger.warning("程序中检查到您数据库开关为关闭状态,已为您跳过删除sql: %s", i)
316 |
--------------------------------------------------------------------------------
/utils/times_tool/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hupangs/api_test/89e9f6d61f3d44342620fcf527180daa1191feea/utils/times_tool/__init__.py
--------------------------------------------------------------------------------
/utils/times_tool/time_control.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | """
5 |
6 | import time
7 | from typing import Text
8 | from datetime import datetime
9 |
10 |
11 | def count_milliseconds():
12 | """
13 | 计算时间
14 | :return:
15 | """
16 | access_start = datetime.now()
17 | access_end = datetime.now()
18 | access_delta = (access_end - access_start).seconds * 1000
19 | return access_delta
20 |
21 |
22 | def timestamp_conversion(time_str: Text) -> int:
23 | """
24 | 时间戳转换,将日期格式转换成时间戳
25 | :param time_str: 时间
26 | :return:
27 | """
28 |
29 | try:
30 | datetime_format = datetime.strptime(str(time_str), "%Y-%m-%d %H:%M:%S")
31 | timestamp = int(
32 | time.mktime(datetime_format.timetuple()) * 1000.0
33 | + datetime_format.microsecond / 1000.0
34 | )
35 | return timestamp
36 | except ValueError as exc:
37 | raise ValueError('日期格式错误, 需要传入得格式为 "%Y-%m-%d %H:%M:%S" ') from exc
38 |
39 |
40 | def time_conversion(time_num: int):
41 | """
42 | 时间戳转换成日期
43 | :param time_num:
44 | :return:
45 | """
46 | if isinstance(time_num, int):
47 | time_stamp = float(time_num / 1000)
48 | time_array = time.localtime(time_stamp)
49 | other_style_time = time.strftime("%Y-%m-%d %H:%M:%S", time_array)
50 | return other_style_time
51 |
52 |
53 | def now_time():
54 | """
55 | 获取当前时间, 日期格式: 2021-12-11 12:39:25
56 | :return:
57 | """
58 | localtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
59 | return localtime
60 |
61 |
62 | def now_time_day():
63 | """
64 | 获取当前时间, 日期格式: 2021-12-11
65 | :return:
66 | """
67 | localtime = time.strftime("%Y-%m-%d", time.localtime())
68 | return localtime
69 |
70 |
71 | def get_time_for_min(minute: int) -> int:
72 | """
73 | 获取几分钟后的时间戳
74 | @param minute: 分钟
75 | @return: N分钟后的时间戳
76 | """
77 | return int(time.time() + 60 * minute) * 1000
78 |
79 |
80 | def get_now_time() -> int:
81 | """
82 | 获取当前时间戳, 整形
83 | @return: 当前时间戳
84 | """
85 | return int(time.time()) * 1000
86 |
--------------------------------------------------------------------------------