├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 # 变量名
--------------------------------------------------------------------------------