├── .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 | ![image](https://user-images.githubusercontent.com/121281024/219560207-1a0364d7-d89d-4fbe-b8c1-822dd3fbd123.png) 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" ![screenshot](" \ 139 | f"https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" \ 140 | 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 "" in line: 58 | line = line.replace("", "") 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 | --------------------------------------------------------------------------------