├── .idea ├── .gitignore ├── API_Pytest.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── common ├── __init__.py ├── com_assert.py ├── com_config.py ├── com_log.py ├── com_manage.py ├── com_params.py ├── com_request.py ├── com_shell.py └── com_yml.py ├── config ├── __init__.py └── config.ini ├── library.txt ├── log └── __init__.py ├── readme.md ├── report └── __init__.py ├── run.py └── testecase ├── __init__.py ├── case ├── __init__.py ├── conftest.py └── test_test.py └── params ├── __init__.py └── test.yaml /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/API_Pytest.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 15:46 4 | """ 5 | -------------------------------------------------------------------------------- /common/com_assert.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/4 9:29 4 | """ 5 | import json 6 | from jsonpath_rw import parse 7 | import logging 8 | import allure 9 | 10 | from common.com_log import ComLog 11 | 12 | """ 13 | 自定义assert 14 | # 类型 15 | # eq_list = [] # 相等 16 | # contains_list = [] # 包含 17 | # not_contains_list = [] # 不包含 18 | # lt_list = [] # 小于 19 | # le_list = [] # 小于等于 20 | # gt_list = [] # 大于 21 | # ge_list = [] # 大于等于 22 | # sw_list = [] # 以xx开头 23 | # ew_list = [] # 以xx结尾 24 | """ 25 | 26 | 27 | class ComAssert: 28 | ComLog().use_log() 29 | 30 | def eq(self, ex, re): 31 | try: 32 | assert str(ex) == str(re) 33 | return True 34 | except Exception as es: 35 | logging.error(F"eq判断失败,预期结果:{ex},实际结果:{re}") 36 | raise (F"eq相等判断失败,预期结果:{ex},实际结果:{re}") 37 | 38 | def contains(self, ex, re): 39 | try: 40 | assert str(ex) in str(re) 41 | return True 42 | except Exception as es: 43 | logging.error(F"contains判断失败,预期结果:{ex},实际结果:{re}") 44 | raise (F"contains判断失败,预期结果:{ex},实际结果:{re}") 45 | 46 | def not_contains(self, ex, re): 47 | try: 48 | assert str(ex) not in str(re) 49 | return True 50 | except Exception as es: 51 | logging.error(F"not_contains判断失败,预期结果:{ex},实际结果:{re}") 52 | raise (F"not_contains判断失败,预期结果:{ex},实际结果:{re}") 53 | 54 | def lt(self, ex, re): 55 | try: 56 | if isinstance(re, int) or isinstance(re, float): 57 | assert float(ex) < float(re) 58 | else: 59 | assert float(ex) < float(len(re)) 60 | return True 61 | except Exception as es: 62 | logging.error(F"lt判断失败,预期结果:{ex},实际结果:{re}") 63 | raise (F"lt判断失败,预期结果:{ex},实际结果:{re}") 64 | 65 | def le(self, ex, re): 66 | try: 67 | # assert float(ex) <= float(re) 68 | if isinstance(re, int) or isinstance(re, float): 69 | assert float(ex) <= float(re) 70 | else: 71 | assert float(ex) <= float(len(re)) 72 | return True 73 | except Exception as es: 74 | logging.error(F"le判断失败,预期结果:{ex},实际结果:{re}") 75 | raise (F"le判断失败,预期结果:{ex},实际结果:{re}") 76 | 77 | def gt(self, ex, re): 78 | try: 79 | # assert float(ex) > float(re) 80 | if isinstance(re, int) or isinstance(re, float): 81 | assert float(ex) > float(re) 82 | else: 83 | assert float(ex) > float(len(re)) 84 | return True 85 | except Exception as es: 86 | logging.error(F"gt判断失败,预期结果:{ex},实际结果:{re}") 87 | raise (F"gt判断失败,预期结果:{ex},实际结果:{re}") 88 | 89 | def ge(self, ex, re): 90 | try: 91 | # assert float(ex) >= float(re) 92 | if isinstance(re, int) or isinstance(re, float): 93 | assert float(ex) >= float(re) 94 | else: 95 | assert float(ex) >= float(len(re)) 96 | return True 97 | except Exception as es: 98 | logging.error(F"ge判断失败,预期结果:{ex},实际结果:{re}") 99 | raise (F"ge判断失败,预期结果:{ex},实际结果:{re}") 100 | 101 | def sw(self, ex, re): 102 | try: 103 | assert str(ex).startswith(str(re)) 104 | return True 105 | except Exception as es: 106 | logging.error(F"sw判断失败,预期结果:{ex},不以{re}开头") 107 | raise (F"sw判断失败,预期结果:{ex},不以{re}开头") 108 | 109 | def ew(self, ex, re): 110 | try: 111 | assert str(ex).endswith(str(re)) 112 | return True 113 | except Exception as es: 114 | logging.error(F"ew判断失败,预期结果:{ex},不以{re}结尾") 115 | raise (F"ew判断失败,预期结果:{ex},不以{re}结尾") 116 | 117 | def assert_result(self, assert_type, ex, re): 118 | """ 119 | 根据判断类型调用对应的判断方法 120 | :param assert_type: 121 | :param ex: 122 | :param re: 123 | :return: 124 | """ 125 | try: 126 | if assert_type == "eq": 127 | return self.eq(ex, re) 128 | elif assert_type == "contains": 129 | return self.contains(ex, re) 130 | elif assert_type == "not_contains": 131 | return self.not_contains(ex, re) 132 | elif assert_type == "lt": 133 | return self.lt(ex, re) 134 | elif assert_type == "not_contains": 135 | return self.not_contains(ex, re) 136 | elif assert_type == "le": 137 | return self.le(ex, re) 138 | elif assert_type == "gt": 139 | return self.gt(ex, re) 140 | elif assert_type == "ge": 141 | return self.ge(ex, re) 142 | elif assert_type == "sw": 143 | return self.sw(ex, re) 144 | elif assert_type == "ew": 145 | return self.ew(ex, re) 146 | except Exception as es: 147 | logging.error(F"出现了非法的比较类型或者比较结果False:{assert_type}") 148 | raise (F"出现了非法的比较类型或者比较结果False:{assert_type}") 149 | 150 | def assert_code(self, assert_type, ex, response): 151 | """ 152 | 判断response.status_code是否如预期 153 | :param assert_type: 154 | :param ex: 155 | :param response: 156 | :return: 157 | """ 158 | try: 159 | ex_code = ex[1] 160 | re_code = response.status_code 161 | return self.assert_result(assert_type, ex_code, re_code) 162 | except Exception as es: 163 | logging.error(F"code判断失败,判断类型是{assert_type}, 预期值:{ex_code}, 实际值:{re_code}") 164 | raise (F"code判断失败,判断类型是{assert_type}, 预期值:{ex_code}, 实际值:{re_code}") 165 | 166 | def assert_headers(self, assert_type, ex_value, response): 167 | """ 168 | 判断headers的内容是否如预期 169 | :param assert_type: 170 | :param ex_value: 171 | :param response: 172 | :return: 173 | """ 174 | try: 175 | ex_headers = ex_value[1] 176 | headers_key = str(ex_value[0]).split(".")[1] 177 | re_headers = response.headers[headers_key] 178 | return self.assert_result(assert_type, ex_headers, re_headers) 179 | except Exception as es: 180 | logging.error( 181 | F"headers判断失败,判断类型是{assert_type}, 预期值:{ex_headers}, headers的key是:{headers_key},实际值:{re_headers}") 182 | raise (F"headers判断失败,判断类型是{assert_type}, 预期值:{ex_headers}, headers的key是:{headers_key},实际值:{re_headers}") 183 | 184 | @staticmethod 185 | def dict_value(key, data): 186 | """ 187 | 根据key层级获取data对应的value;jsonpath_rw的方法也可以实现(jsonpath-rw无法安装时用) 188 | :param key: str:"one.two.three..." 189 | :param data: dict 190 | :return: 191 | """ 192 | 193 | key_list = str(key).split(".") 194 | for now_key in key_list: 195 | if len(key_list) == 1: 196 | value = data[now_key] 197 | return value 198 | else: 199 | new_data = data[now_key] 200 | key = str.join(".", key_list[1:]) 201 | return ComAssert.dict_value(key, new_data) # 递归别忘了返回 202 | 203 | def assert_content(self, assert_type, ex_value, response): 204 | """ 205 | 判断content的内容是否如预期 206 | :param assert_type: 207 | :param ex_value: 208 | :param response: 209 | :return: 210 | """ 211 | try: 212 | # 把"content.xx.yy"转成["xx.yy"]再转成"xx.yy" 213 | keys = str(ex_value[0].split("content.")[1:][0]) 214 | ex_content = ex_value[1] 215 | 216 | json_content = json.loads(response.content) # byte转dict 217 | # jsonpath_rw,根据key("xx.yy.zz"),返回dict中该key的值 218 | re_content = [match.value for match in parse(str(keys)).find(json_content)][0] 219 | return self.assert_result(assert_type, ex_content, re_content) 220 | except Exception as es: 221 | logging.error( 222 | F"content判断失败或者响应文本不是json格式,判断类型是{assert_type}, 预期值:{ex_content}, content的key是:{keys},实际值:{re_content}") 223 | raise ( 224 | F"content判断失败或者响应文本不是json格式,判断类型是{assert_type}, 预期值:{ex_content}, content的key是:{keys},实际值:{re_content}") 225 | -------------------------------------------------------------------------------- /common/com_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/2 17:32 4 | """ 5 | import time 6 | from configparser import ConfigParser 7 | import os 8 | 9 | """ 10 | 封装config配置文件的方法 11 | """ 12 | 13 | 14 | class ComConfig: 15 | # section 16 | TEST_PATH = "test_path" 17 | 18 | # test_path 19 | PARAMS_FOLDER_PATH = "params_folder_path" 20 | 21 | def __init__(self): 22 | self.cp = ConfigParser() 23 | self.base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 24 | config_path = os.path.join(self.base_path, "config", "config.ini") 25 | self.cp.read(config_path, encoding='utf-8') 26 | 27 | def __get_value(self, section, option): 28 | return self.cp.get(section, option) 29 | 30 | def test_params_path(self): 31 | # print(F"path:{os.path.join(self.base_path, self.__get_value(self.TEST_PATH, self.PARAMS_FOLDER_PATH))}") 32 | 33 | return os.path.join(self.base_path, self.__get_value(self.TEST_PATH, self.PARAMS_FOLDER_PATH)) 34 | 35 | def get_report_path(self): 36 | xml_dir_path = os.path.join("report", time.strftime("%m-%d-%H") + "\\xml") 37 | html_dir_path = os.path.join("report", time.strftime("%m-%d-%H") + "\\html") 38 | 39 | xml_report_path = os.path.join(self.base_path, xml_dir_path) 40 | html_report_path = os.path.join(self.base_path, html_dir_path) 41 | return xml_report_path, html_report_path 42 | -------------------------------------------------------------------------------- /common/com_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 16:36 4 | """ 5 | 6 | import time 7 | import logging 8 | # from pathlib import Path, PurePath # PurePath用于拼接路径 9 | import os 10 | 11 | """ 12 | 设置日志文件的基本属性 13 | """ 14 | 15 | 16 | class ComLog: 17 | func = None 18 | 19 | def __new__(cls, *args, **kwargs): 20 | if not cls.func: 21 | cls.func = super().__new__(cls) 22 | return cls.func 23 | return cls.func 24 | 25 | def use_log(self, log_level=logging.INFO): 26 | # log_path = PurePath(str(Path.r(target=__file__)), "log", str(time.strftime('%m_%d', time.localtime())) + '_error.log') 27 | log_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "log", 28 | str(time.strftime('%m_%d', time.localtime())) + '_error.log') 29 | logging.basicConfig( 30 | filename=log_path, 31 | filemode='a+', 32 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s ==> %(message)s', # 内容格式;单词错误的话,message报错 33 | datefmt='%Y-%m-%d %H:%M:%S', 34 | level=log_level 35 | ) 36 | 37 | # 设置编码 38 | encode_header = logging.FileHandler(log_path, encoding='utf-8') 39 | logging.getLogger(str(log_path)).addHandler(encode_header) 40 | -------------------------------------------------------------------------------- /common/com_manage.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/3 15:17 4 | """ 5 | 6 | import logging 7 | 8 | from common.com_log import ComLog 9 | from common.com_request import ComRequest 10 | from common.com_assert import ComAssert 11 | from common.com_config import ComConfig 12 | from common.com_params import ComParams 13 | 14 | 15 | """ 16 | 对请求数据进行判断,并调用ComParams进行数据的处理 17 | """ 18 | 19 | 20 | class ComManage: 21 | ComLog().use_log() 22 | 23 | def __init__(self): 24 | self.com_assert = ComAssert() 25 | self.config = ComConfig() 26 | self.com_params = ComParams() 27 | 28 | def validate_manner(self, validates_dict): 29 | """ 30 | 处理用例数据中的validates数据 31 | :param validates_dict: 32 | :return: 33 | """ 34 | try: 35 | ex_dict = dict() 36 | 37 | # 类型 38 | eq_list = [] # 相等 39 | contains_list = [] # 包含 40 | not_contains_list = [] # 不包含 41 | lt_list = [] # 小于 42 | le_list = [] # 小于等于 43 | gt_list = [] # 大于 44 | ge_list = [] # 大于等于 45 | sw_list = [] # 以xx开头 46 | ew_list = [] # 以xx结尾 47 | 48 | for validate in eval(validates_dict): 49 | if "eq" in validate: 50 | eq_list.append(validate["eq"]) 51 | elif "contains" in validate: 52 | contains_list.append(validate["contains"]) 53 | elif "not_contains" in validate: 54 | not_contains_list.append(validate["not_contains"]) 55 | elif "lt" in validate: 56 | lt_list.append(validate["lt"]) 57 | elif "le" in validate: 58 | le_list.append(validate["le"]) 59 | elif "gt" in validate: 60 | gt_list.append(validate["gt"]) 61 | elif "ge" in validate: 62 | ge_list.append(validate["ge"]) 63 | elif "sw" in validate: 64 | sw_list.append(validate["sw"]) 65 | elif "ew" in validate: 66 | ew_list.append(validate["ew"]) 67 | 68 | ex_dict["eq"] = eq_list 69 | ex_dict["contains"] = contains_list 70 | ex_dict["not_contains"] = not_contains_list 71 | ex_dict["lt"] = lt_list 72 | ex_dict["le"] = le_list 73 | ex_dict["gt"] = gt_list 74 | ex_dict["ge"] = ge_list 75 | ex_dict["sw"] = sw_list 76 | ex_dict["ew"] = ew_list 77 | 78 | return ex_dict 79 | 80 | except Exception as es: 81 | logging.error(F"解析validate数据失败,数据为:{validates_dict}") 82 | raise es 83 | 84 | def request_manner(self, request_dict): 85 | """ 86 | 发送请求,同时处理依赖数据: 87 | 88 | 处理variables、variables_data、relevance 89 | 执行variables对应的用例,返回响应中relevance对应的值,并赋值给variables_data 90 | 替换测试用例parameters中对应的variables_data。然后返回处理过后的parameters 91 | 92 | 1:先确定需要执行的用例yml中的请求(因为一个yml会存在多个请求内容) 93 | 2:判断variables_data的值,在对应用例的param中的relevance的value, 94 | 3:再从该用例响应中提取出来 95 | 4:接着替换掉之前的测试用例parameters中对应的variables_data。然后返回处理过后的parameters 96 | 97 | :param request_dict: 98 | :return: 99 | """ 100 | # variables: [{'login': ['basic_id', 'audid']}, {'Basic': ['test_id']}] 101 | # variables_data: ['audid', 'basic_id'] 102 | # relevance: {'id': 'content.data'} 103 | 104 | try: 105 | if "variables" in request_dict and "variables_data" in request_dict: 106 | 107 | # 处理variables的内容,去掉$,方便处理 108 | variables_data = request_dict["variables_data"] 109 | variables = request_dict["variables"] 110 | yaml_path = self.config.test_params_path() 111 | 112 | for variable in eval(variables): 113 | # 获取依赖数据的值 114 | for test_name in variable: 115 | # print(F"variablesss:{variable}") 116 | yaml_name = str(test_name) + ".yml" 117 | variables_value = self.variables_value(yaml_path, yaml_name, variables_data) 118 | # 把依赖数据变量替换依赖数据的值 119 | request_dict = self.com_params.replace_request(request_dict, variables_value) 120 | response = ComRequest().send_request(request_dict) 121 | return response 122 | except Exception as e: 123 | logging.error(F"请求发送失败,请求信息是:{request_dict}") 124 | raise (F"请求发送失败,请求信息是:{request_dict}") 125 | 126 | # 因为调用了Mannage类的的方法,两个类之间不能互相调用,所以移来这里 127 | def variables_value(self, yaml_path, yaml_name, variables_data): 128 | """ 129 | 获取依赖数据的值。比如 130 | variables: [{'login': ['data_value', 'code_value']}, {'Basic': ['test_id']}] 131 | variables_data: ['data_value', 'code_value'] 132 | relevance: {'data_value': 'content.data', 'code_value': 'content.code'} 133 | 根据variables,到对应的测试用例yaml(key)中,获取到其中拥有 134 | 和variables的value全部一致的relevance的key的测试用例信息 135 | 然后执行该测试用例,根据relevance的value,到respon中获取对应的值(依赖测试数据的值) 136 | :param yaml_path: 137 | :param yaml_name: 138 | :param variables_data: 139 | :return: 140 | """ 141 | try: 142 | tests_params = ComParams().test_params(yaml_path, yaml_name)[0] # 获取到想要执行用例yml的所有内容 143 | num = 0 # 记录依赖测试用例的请求执行次数,只需要执行一次即可 144 | for test_params in tests_params: 145 | if "relevance" in test_params: # 判断存在relevance的请求信息内容 146 | relevance_data = test_params["relevance"] 147 | variables_value = [] 148 | 149 | if self.com_params.list_in_dict(variables_data, eval(relevance_data)): # 获取有全部variables_data的请求信息内容 150 | response = ComManage().request_manner(test_params) 151 | num += 1 152 | for variable_key in variables_data: 153 | # 在relevance_data中,获取variables_data的key对应的value 154 | variable_dict = self.com_params.relevance_value(variable_key, 155 | eval(relevance_data)[variable_key], 156 | response) 157 | variables_value.append(variable_dict) 158 | if num >= 1: 159 | return variables_value 160 | except Exception as es: 161 | logging.error(F"被依赖用例{yaml_name}不存在依赖用例所需的依赖数据:{variables_data}") 162 | raise (F"被依赖用例{yaml_name}不存在依赖用例所需的依赖数据:{variables_data}") 163 | 164 | def assert_manner(self, request_dict): 165 | """ 166 | 根据测试用例的validate部分的判断类型(content/headers/status_code),调用不同的判断方法 167 | :param request_dict: 168 | :return: 全通过则返回True 169 | """ 170 | try: 171 | # params = request_dict[0][0] 172 | # ex_validates = self.validate_manner(params["validate"]) 173 | # response = self.request_manner(params) 174 | 175 | ex_validates = self.validate_manner(request_dict["validate"]) 176 | response = self.request_manner(request_dict) 177 | 178 | for key in ex_validates: 179 | values = ex_validates[key] 180 | 181 | if len(values) >= 1: 182 | asssert_type = key 183 | for value in values: 184 | value_start = value[0].split(".")[0] 185 | if value_start.startswith("status_code"): 186 | assert self.com_assert.assert_code(asssert_type, value, response) 187 | elif value_start.startswith("headers"): 188 | assert self.com_assert.assert_headers(asssert_type, value, response) 189 | elif value_start.startswith("content"): 190 | assert self.com_assert.assert_content(asssert_type, value, response) 191 | return True 192 | 193 | except Exception as es: 194 | logging.error(F"异常判断类型:{key},判断值是{value},响应数据是{response.content}") 195 | raise (F"异常判断类型:{key},判断值是{value},响应数据是{response.content}") 196 | 197 | if __name__ == '__main__': 198 | 199 | yaml_path = ComConfig().test_params_path() 200 | data = ComParams().test_params(yaml_path, yaml_name="test.yml")[0][0] 201 | value = ComManage().assert_manner(data[0]) 202 | # value = ComManage().assert_manner(data) 203 | # # value = ComManage().relevance_request_manner(data) 204 | # print(value) 205 | 206 | 207 | -------------------------------------------------------------------------------- /common/com_params.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/2 10:46 4 | """ 5 | import json 6 | import logging 7 | import re 8 | from pathlib import PurePath 9 | from jsonpath_rw import parse 10 | 11 | from common.com_yml import ComYaml 12 | from common.com_log import ComLog 13 | 14 | """ 15 | 被ComManage调用,对请求数据进行处理 16 | """ 17 | 18 | 19 | class ComParams(object): 20 | 21 | def __init__(self): 22 | ComLog().use_log() 23 | 24 | def yaml_params(self, yml_params_path): 25 | """ 26 | 格式处理yml测试用例的数据 27 | :param yml_params_path: 28 | :return: {"yaml测试用例dec标题_0": url:xx, method:xx, data:xxx, header:xxx, relevance:xxx, variables:[{xx:yy}], variables_data:[xx,xx]}, {},...} 29 | """ 30 | params_title = list() 31 | datas = ComYaml().read_yaml(yml_params_path) 32 | param_value = {} 33 | try: 34 | for data in [x for x in datas.items()]: 35 | params = {} 36 | pytest_values = list() # 为了提供符合parametrize的数据 37 | 38 | value = data[1] 39 | parameters = value["parameters"] 40 | i = 0 41 | # 一个用例文件可能有多个parameter,即多个请求 42 | for parameter in parameters: 43 | i += 1 44 | 45 | # 标题 46 | title = str(parameter["title"]) 47 | 48 | # url 49 | url = str(parameter["url"]) 50 | if url.startswith("http") or url.startswith("https"): 51 | param_value["url"] = url 52 | if not url.startswith("/"): 53 | url = "/" + url 54 | param_value["url"] = value["host"] + url 55 | 56 | # method 57 | method = str(parameter["method"]) 58 | param_value["method"] = method 59 | 60 | # data 61 | re_data = str(parameter["data"]) 62 | param_value["data"] = re_data 63 | 64 | # header 65 | # header = str(parameter["header"]) 66 | header = data[1]['header'] 67 | param_value["header"] = header 68 | 69 | # validate 70 | validate = str(parameter["validate"]) 71 | param_value["validate"] = validate 72 | 73 | # 非必须:提取依赖数据(前置用例) 74 | if "relevance" in parameter: 75 | relevance = str(parameter["relevance"]) 76 | param_value["relevance"] = relevance 77 | 78 | # 非必须:依赖数据(后置用例),获取parameter中所有以$开头的字段 79 | variables_data = re.findall("\$[A-za-z0-9]+", str(parameter)) 80 | if variables_data: 81 | values = [] 82 | for j in variables_data: 83 | values.append(j.split("$")[1]) 84 | param_value["variables_data"] = values 85 | 86 | # 非必须:指定依赖数据的来源用例以及对应的依赖数据变量名 87 | if "variables" in parameter: 88 | variables = str(parameter["variables"]) 89 | param_value["variables"] = variables 90 | 91 | # 键值对 name_i:param_value 92 | key = data[0] + F"_{i}" 93 | params[key] = param_value 94 | 95 | # 组成param 96 | params_title.append(params) 97 | params_title.append(title) 98 | pytest_values.append(params_title) 99 | # 开辟新的内存地址 100 | params_title = list() 101 | params = dict() 102 | param_value = dict() 103 | 104 | # 不能使用这个。得到的结果是params_title中params的值为空,因为内存地址相同 105 | # params.pop(key) 106 | 107 | except Exception as es: 108 | logging.error(F"格式处理yml测试用例的数据报错,报错的数据是:{parameter},错误信息:{es}") 109 | raise (F"格式处理yml测试用例的数据报错,报错的数据是:{parameter},错误信息:{es}") 110 | return pytest_values 111 | # return params_title 112 | # return params 113 | 114 | def test_params(self, yaml_path, yaml_name): 115 | """ 116 | 获取指定yaml测试文件的数据 (为了配合pytest的pytest.mark.parametrize;对应test_xx.py) 117 | :param yaml_path: yaml所在文件夹路径 118 | :param yaml_name: yaml文件名称 119 | :return: 120 | """ 121 | try: 122 | if yaml_name.endswith(".yml"): 123 | file_path = PurePath(yaml_path, yaml_name) 124 | params_titles = ComParams().yaml_params(file_path) 125 | 126 | new_params_titles = list() 127 | param_title = list() 128 | 129 | for values in params_titles: 130 | old_params = values[0] 131 | for key in old_params: 132 | new_param = old_params[key] 133 | param_title.append(new_param) 134 | param_title.append(values[1]) 135 | new_params_titles.append(param_title) 136 | param_title = list() 137 | 138 | return new_params_titles 139 | except Exception as es: 140 | logging.error(F"yaml文件解析出错,路径是:{yaml_path}, 文件名是:{yaml_name}") 141 | raise (F"yaml文件解析出错,路径是:{yaml_path}, 文件名是:{yaml_name}") 142 | 143 | def list_in_dict(self, variables_list, dict): 144 | """ 145 | 判断['key1', 'key2'...]里的所有key,是否都一一对应dict的key 146 | 比如['key1', 'key2'...], {'key1':'value1', 'key2':'value2'}则true 147 | 如果是 {'key1':'value1', 'key2':'value2', 'key3': 'value3'}则false 148 | :param variables_list: 149 | :param dict: 150 | :return: 151 | """ 152 | try: 153 | len_variables = len(variables_list) 154 | for variable in variables_list: 155 | if len_variables == 1: 156 | if variable in dict and len(dict) == 1: 157 | return True 158 | else: 159 | return False 160 | if variable in dict: 161 | new_variables_list = variables_list[1:] 162 | dict.pop(variable) 163 | return self.list_in_dict(new_variables_list, dict) 164 | else: 165 | logging.error(F"依赖数据获取用例中的relevance_data:{dict}中,没有全部对应或存在重复的key:{variables_list}") 166 | return False 167 | except Exception as es: 168 | logging.error(F"依赖数据获取用例中的relevance_data:{dict}中,没有全部对应或存在重复的key:{variables_list}") 169 | raise (F"依赖数据获取用例中的relevance_data:{dict}中,没有全部对应或存在重复的的key:{variables_list}") 170 | 171 | def content_to_json(self, response): 172 | try: 173 | json_response = json.loads(response.content) 174 | return json_response 175 | except Exception as es: 176 | logging.error(F"响应文本不是json格式, 响应文本是:{response.text}") 177 | raise (F"响应文本不是json格式, 响应文本是:{response.text}") 178 | 179 | def relevance_value(self, variable_key, variable_data, response): 180 | """ 181 | 根据key,获取response.content的值,,返回[{}, {}] 182 | :param variable_data: 183 | :param response: 184 | :return: 185 | """ 186 | try: 187 | variable_dict = dict() 188 | # 把"content.xx.yy"转成["xx.yy"]再转成"xx.yy" 189 | keys = str(variable_data.split("content.")[1:][0]) 190 | json_content = self.content_to_json(response) # byte转dict 191 | # jsonpath_rw,根据key("xx.yy.zz"),返回dict中该key的值 192 | re_content = [match.value for match in parse(str(keys)).find(json_content)][0] 193 | variable_dict[variable_key] = re_content 194 | return variable_dict 195 | except Exception as es: 196 | logging.error(F"依赖数据提取出错,想要提取的值:{variable_data}, 响应content是{response.content}") 197 | raise (F"依赖数据提取出错,想要提取的值:{variable_data}, 响应content是{response.content}") 198 | 199 | def replace_request(self, request_dict, variables_value): 200 | """ 201 | 把请求信息中的依赖数据参数,替换成对应的依赖数据的值 202 | :param request_dict: 203 | :param variables_value: 204 | :return: 205 | """ 206 | 207 | try: 208 | # ['data_value', 'code_value'] 209 | variable_keys = [ 210 | str(key).split("$")[1] 211 | for key in re.findall("\$[A-za-z0-9]+", str(request_dict))] 212 | 213 | i = 0 214 | for variable_dict in variables_value: 215 | variable_key = variable_keys[i] 216 | if variable_key in variable_dict: 217 | # print(F"un_request_dict:{request_dict}") 218 | request_dict = str(request_dict).replace(F"${variable_key}", str(variable_dict[variable_key])) 219 | i += 1 220 | try: 221 | request_dict = eval(request_dict) 222 | except Exception as es: 223 | logging.error(F"依赖数据替换后的请求信息无法转成dict,请求信息:{request_dict}") 224 | raise (F"依赖数据替换后的请求信息无法转成dict,请求信息:{request_dict}") 225 | return request_dict 226 | except Exception as es: 227 | logging.error(F"请求信息中的依赖数据替换失败,只能提取content.的数据,并且依赖数据的值不能是字典类型,依赖数据是:{variables_value}," 228 | F"\n请求数据是:{request_dict}") 229 | raise (F"请求信息中的依赖数据替换失败,只能提取content.的数据,并且依赖数据的值不能是字典类型,依赖数据是:{variables_value}," 230 | F"\n请求数据是:{request_dict}") 231 | -------------------------------------------------------------------------------- /common/com_request.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/2 9:36 4 | """ 5 | 6 | import requests 7 | import json 8 | import logging 9 | from common.com_log import ComLog 10 | import urllib3 11 | import allure 12 | 13 | """ 14 | requests常用方法 15 | 只支持http/https,以及get、post请求 16 | """ 17 | 18 | # 忽略InsecureRequestWarning 19 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 20 | ComLog().use_log() 21 | 22 | 23 | class ComRequest: 24 | req_session = requests.session() 25 | 26 | def send_request(self, request_data): 27 | global response 28 | try: 29 | self.url = request_data["url"].strip() 30 | except Exception as es: 31 | logging.error(F"请求数据的url获取失败,请求数据是:{request_data},错误是{es}") 32 | raise (F"请求数据的url获取失败,请求数据是:{request_data},错误是{es}") 33 | 34 | try: 35 | method = request_data["method"] 36 | header = request_data["header"] 37 | # yaml读取过来的json数据是[{}}]中字符串被'括起,需要替换成" 38 | data = self.is_json(request_data["data"]) 39 | # data = eval(request_data['data'].replace("'", '"')) 40 | # data = json.loads(request_data['data'].replace("'", '"')) 41 | # if "json" in data.keys(): # 判断是否是json格式(测试用例中指定json) 42 | # data = json.dumps(data["json"]) 43 | # else: 44 | # data = json.dumps(data) # 非json格式则转回str 45 | if method == 'get': 46 | if not data.startswith("?"): 47 | url = self.url + "?" + data 48 | 49 | allure.attach(name="请求地址:", body=F"{url}") 50 | allure.attach(name="请求方法", body=F"{method}") 51 | allure.attach(name="请求头:", body=F"{header}") 52 | allure.attach(name="请求data:", body=F"{data}") 53 | response = self.req_session.get(url=url, headers=header, params=data, 54 | verify=False) 55 | elif method == 'post': 56 | allure.attach(name="请求地址:", body=F"{self.url}") 57 | allure.attach(name="请求方法", body=F"{method}") 58 | allure.attach(name="请求头:", body=F"{header}") 59 | allure.attach(name="请求data:", body=F"{data}") 60 | response = self.req_session.post(url=self.url, data=data, headers=header, 61 | verify=False) 62 | 63 | except Exception as e: 64 | logging.error(F'发送请求失败,请求url是:{self.url},发生的错误是:{e}') 65 | raise (F'发送请求失败,请求url是:{self.url},发生的错误是:{e}') 66 | 67 | return response 68 | 69 | def is_json(self, str): 70 | """ 71 | 判断data是否需要以json格式进行请求 72 | :param str: 73 | :return: 74 | """ 75 | try: 76 | # yaml读取过来的json数据是[{}}]中字符串被'括起,需要替换成" 77 | # 同时转成json 78 | json_data = json.loads(str.replace("'", '"')) 79 | if "json" in json_data.keys(): # 判断是否是json格式(测试用例中指定json) 80 | data = json.dumps(json_data["json"]) 81 | else: 82 | data = json.dumps(json_data) # 非json格式则转回str 83 | return data 84 | except: 85 | # 如果非json格式,则返回值 86 | return str 87 | -------------------------------------------------------------------------------- /common/com_shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/9 17:40 4 | """ 5 | import subprocess 6 | 7 | """ 8 | 执行shell语句的封装 9 | """ 10 | 11 | class ComShell: 12 | 13 | def invoke(self, cmd): 14 | output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 15 | o = output.decode("utf-8") 16 | return o 17 | -------------------------------------------------------------------------------- /common/com_yml.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/2 09:27 4 | """ 5 | import logging 6 | 7 | import yaml 8 | from common.com_log import ComLog 9 | from pathlib import Path 10 | 11 | 12 | """ 13 | 封装操作yaml的read 14 | """ 15 | 16 | class ComYaml: 17 | 18 | ComLog().use_log() 19 | 20 | @staticmethod 21 | def __read_yaml(yaml_path): 22 | """ 23 | 读取yaml文件 24 | :param yaml_path: yaml文件路径 25 | :return: {} 26 | """ 27 | try: 28 | dict_data = yaml.load(open(yaml_path, 'r', encoding="utf-8"), Loader=yaml.FullLoader) 29 | return dict_data 30 | except Exception as es: 31 | logging.error(F'读取{yaml_path}文件出错,错误是{es}') 32 | raise (F'读取{yaml_path}文件出错,错误是{es}') 33 | 34 | def read_yaml(self, yml_path): 35 | """ 36 | 遍历文件夹下的所有yaml文件,拆分其中的dec和parameters,并设置为字典的键值对,返回字典 37 | :param yml_path: yaml文件路径 或 yaml文件路径 38 | :return: {dec1:parameters1, dec2:parameters2} 39 | """ 40 | values_dict = {} 41 | # 判断路径是否是文件夹、获取该文件夹下所有的yml文件、并遍历 42 | try: 43 | if Path(yml_path).is_dir(): 44 | for file in [x for x in list(Path(yml_path).glob("**/*.yml"))]: 45 | data_dict = self.__read_yaml(file) 46 | for test_name, parameters in data_dict.items(): 47 | values_dict[test_name] = parameters 48 | elif str(yml_path).endswith(".yml"): 49 | file = yml_path # 为了log服务才这样赋值的 50 | data_dict = self.__read_yaml(file) 51 | for test_name, parameters in data_dict.items(): 52 | values_dict[test_name] = parameters 53 | except Exception as es: 54 | logging.error(F"解析{file}文件内容出错,错误是{es}") 55 | raise Exception 56 | return values_dict 57 | 58 | 59 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 15:49 4 | """ 5 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | ; 测试环境:相对路径配置项 2 | [test_path] 3 | ; yml测试用例所在文件夹 4 | params_folder_path = testecase/params -------------------------------------------------------------------------------- /library.txt: -------------------------------------------------------------------------------- 1 | allure-pytest==2.8.6 2 | allure-python-commons==2.8.6 3 | apipkg==1.5 4 | atomicwrites==1.4.0 5 | attrs==19.3.0 6 | certifi==2020.4.5.2 7 | chardet==3.0.4 8 | colorama==0.4.3 9 | decorator==4.4.2 10 | execnet==1.7.1 11 | idna==2.9 12 | importlib-metadata==1.6.1 13 | jsonpath-rw==1.4.0 14 | more-itertools==8.3.0 15 | pluggy==0.13.1 16 | ply==3.11 17 | py==1.8.1 18 | pytest==4.5.0 19 | pytest-forked==1.1.3 20 | pytest-xdist==1.32.0 21 | PyYAML==5.3.1 22 | requests==2.23.0 23 | six==1.15.0 24 | urllib3==1.25.9 25 | wcwidth==0.2.4 26 | -------------------------------------------------------------------------------- /log/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 15:49 4 | """ 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 使用步骤: 2 | 1、安装依赖库: pip install -r library.txt 3 | 2、在testcase/params路径,编写测试用例 4 | 3、复制testcase/case下的py,重命名test_xx.py,修改其中的类名,以及yaml_name的值为对应的yaml用例名 5 | 4、运行run 6 | 5、测试报告会生成在report路径中按照月-日-时生成的目录下 7 | PS.参考test_test.py、test.yaml 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /report/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/18 11:29 4 | """ 5 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/10 15:08 4 | """ 5 | 6 | import logging 7 | import time 8 | import pytest 9 | import os 10 | 11 | from common.com_log import ComLog 12 | from common.com_config import ComConfig 13 | from common.com_shell import ComShell 14 | 15 | 16 | class Run: 17 | 18 | def __init__(self): 19 | ComLog().use_log() 20 | # 报告文件文件夹,防止生成的报告内容叠加 21 | self.time = time.strftime('%m-%d-%H-%M', time.localtime()) 22 | self.report_path = ComConfig().get_report_path() 23 | 24 | def run_case(self): 25 | # 执行测试 26 | args = ["-s", "-n", "1", "--alluredir", F"{self.report_path[0]}"] 27 | pytest.main(args) 28 | # 生成测试报告 29 | cmd = F"allure generate --clean {self.report_path[0]} -o {self.report_path[1]}" 30 | ComShell().invoke(cmd) 31 | 32 | 33 | if __name__ == '__main__': 34 | Run().run_case() 35 | 36 | -------------------------------------------------------------------------------- /testecase/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 15:49 4 | """ 5 | -------------------------------------------------------------------------------- /testecase/case/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/10 14:12 4 | """ 5 | -------------------------------------------------------------------------------- /testecase/case/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/2 17:26 4 | """ 5 | 6 | import pytest 7 | import allure 8 | from common.com_request import ComRequest 9 | 10 | """ 11 | pytest的conftest文件 12 | """ 13 | 14 | 15 | -------------------------------------------------------------------------------- /testecase/case/test_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/10 14:28 4 | """ 5 | 6 | import allure 7 | import pytest 8 | 9 | from common.com_params import ComParams 10 | from common.com_config import ComConfig 11 | from common.com_log import ComLog 12 | from common.com_manage import ComManage 13 | 14 | 15 | @allure.epic("xxx") 16 | @allure.feature("test接口") # 功能点的描述 17 | class Testtest: 18 | ComLog().use_log() 19 | yaml_path = ComConfig().test_params_path() 20 | # 获取指定测试用例的用例信息 21 | test_params = ComParams().test_params(yaml_path, yaml_name="test.yml") 22 | 23 | @allure.title("{title}") 24 | @pytest.mark.parametrize("param, title", test_params) 25 | def test_login(self, param, title): 26 | result = ComManage().assert_manner(param) 27 | assert result 28 | 29 | 30 | if __name__ == '__main__': 31 | pytest.main(["-s", "test_test.py"]) 32 | -------------------------------------------------------------------------------- /testecase/params/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author:LZL 3 | @Time:2020/6/1 18:32 4 | """ 5 | -------------------------------------------------------------------------------- /testecase/params/test.yaml: -------------------------------------------------------------------------------- 1 | Collections: # 必填 2 | dec: "百度测试" # 非必填 3 | host: https://www.baidu.com # 必填 4 | header: { # 必填 5 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko)\ 6 | Chrome/67.0.3396.99 Safari/537.36", 7 | "Content-Type": "keep-alive" 8 | } 9 | parameters: 10 | - 11 | title: 标题1 # 必填 12 | url: /sugrec/ # 必填 13 | method: get # 必填 14 | # get的data直接写 必填 15 | data: data_value=$data_value&code_value=$code_value 16 | 17 | validate: # 必填 18 | - eq: 19 | - status_code 20 | - 200 21 | 22 | # 非必填 23 | variables: # 需要获取的变量值(本用例需要) 24 | - login: # 提取变量的测试用例 25 | - code_value 26 | - data_value # 变量名 27 | 28 | # 非必填 29 | relevance: # 提取出去的变量(提供其它用例) 30 | id: content.data # 变量名: 变量位置 31 | 32 | - 33 | title: 标题2 34 | url: /users/95c34f9cc50c/collections_and_notebooks/$audid 35 | method: post 36 | data: 37 | json: # 如果data是json格式,需指定。必填 38 | slug1: "95c34f9cc50c" 39 | slug2: "basic_id" 40 | validate: 41 | - eq: 42 | - content.msg 43 | - ok 44 | - contains: 45 | - content.msg 46 | - 失败 47 | variables: # 需要获取的变量值 48 | - login: # 提取变量的测试用例 49 | - basic_id # 变量名 --------------------------------------------------------------------------------