pytest-api-framework-alpha 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- framework/__init__.py +0 -0
- framework/allure_report.py +35 -0
- framework/base_class.py +61 -0
- framework/conftest.py +523 -0
- framework/db/__init__.py +0 -0
- framework/db/mysql_db.py +111 -0
- framework/db/redis_db.py +142 -0
- framework/exit_code.py +19 -0
- framework/extract.py +101 -0
- framework/global_attribute.py +100 -0
- framework/http_client.py +163 -0
- framework/render_data.py +185 -0
- framework/report.py +102 -0
- framework/startapp.py +126 -0
- framework/utils/__init__.py +0 -0
- framework/utils/common.py +211 -0
- framework/utils/encrypt.py +387 -0
- framework/utils/log_util.py +3 -0
- framework/utils/teams_util.py +48 -0
- framework/utils/yaml_util.py +25 -0
- framework/validate.py +207 -0
- pytest_api_framework_alpha-0.1.0.dist-info/METADATA +29 -0
- pytest_api_framework_alpha-0.1.0.dist-info/RECORD +25 -0
- pytest_api_framework_alpha-0.1.0.dist-info/WHEEL +5 -0
- pytest_api_framework_alpha-0.1.0.dist-info/top_level.txt +1 -0
framework/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from framework.utils.log_util import logger
|
|
5
|
+
from framework.global_attribute import CONTEXT
|
|
6
|
+
from config.settings import ALLURE_DIR, ALLURE_REPORT_DIR, ALLURE_RESULTS_DIR, ALLURE_ENV_PROPERTIES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_report():
|
|
10
|
+
set_allure_environment()
|
|
11
|
+
if platform.platform() == "Linux":
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
cmd = f"{ALLURE_DIR} generate {ALLURE_RESULTS_DIR} -o {ALLURE_REPORT_DIR} --clean"
|
|
16
|
+
os.system(cmd)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
logger.error(f"生成allure测试报告异常:{e}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_allure_environment():
|
|
22
|
+
"""生成allure的环境信息"""
|
|
23
|
+
environment = list()
|
|
24
|
+
pl = platform.platform()
|
|
25
|
+
pl = pl[:pl.index("-")]
|
|
26
|
+
environment.append(f"Platform={pl}\n")
|
|
27
|
+
python_version = platform.python_version()
|
|
28
|
+
environment.append(f"Python={python_version}\n")
|
|
29
|
+
allure_version = "2.13.1"
|
|
30
|
+
environment.append(f"Allure={allure_version}\n")
|
|
31
|
+
environment.append(f"Env={CONTEXT.env}\n")
|
|
32
|
+
environment.append(f"App={CONTEXT.app}\n")
|
|
33
|
+
|
|
34
|
+
# with open(ALLURE_ENV_PROPERTIES, "w") as f:
|
|
35
|
+
# f.writelines(environment)
|
framework/base_class.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from box import Box
|
|
5
|
+
|
|
6
|
+
from framework.utils.log_util import logger
|
|
7
|
+
from framework.db.mysql_db import MysqlDB
|
|
8
|
+
from framework.db.redis_db import RedisDB
|
|
9
|
+
from framework.exit_code import ExitCode
|
|
10
|
+
from framework.http_client import ResponseUtil
|
|
11
|
+
from framework.global_attribute import CONFIG, GlobalAttribute
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseTestCase(object):
|
|
15
|
+
context: GlobalAttribute = None
|
|
16
|
+
config: GlobalAttribute = None
|
|
17
|
+
http = None
|
|
18
|
+
data: Box = None
|
|
19
|
+
belong_app = None
|
|
20
|
+
response: ResponseUtil = None
|
|
21
|
+
|
|
22
|
+
def request(self, app=None, *, account, data, **kwargs):
|
|
23
|
+
try:
|
|
24
|
+
app = self.default_app(app)
|
|
25
|
+
app_http = getattr(self.http, app)
|
|
26
|
+
self.response = getattr(app_http, account).request(data=data, keyword=kwargs)
|
|
27
|
+
return self.response
|
|
28
|
+
except AttributeError as e:
|
|
29
|
+
logger.error(f"app {app} or account {account} no exist: {e}")
|
|
30
|
+
traceback.print_exc()
|
|
31
|
+
pytest.exit(ExitCode.APP_OR_ACCOUNT_NOT_EXIST)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def post(self, app, account, url, data=None, json=None, **kwargs):
|
|
35
|
+
return getattr(getattr(self.http, app), account).post(app, url, data=data, json=json, **kwargs)
|
|
36
|
+
|
|
37
|
+
def get(self, app, account, url, params=None, **kwargs):
|
|
38
|
+
return getattr(getattr(self.http, app), account).get(app, url, params=params, **kwargs)
|
|
39
|
+
|
|
40
|
+
def put(self, app, account, url, data=None, **kwargs):
|
|
41
|
+
return getattr(getattr(self.http, app), account).put(app, url, data=data, **kwargs)
|
|
42
|
+
|
|
43
|
+
def delete(self, app, account, url, **kwargs):
|
|
44
|
+
return getattr(getattr(self.http, app), account).delete(app, url, **kwargs)
|
|
45
|
+
|
|
46
|
+
def mysql_conn(self, db, app=None):
|
|
47
|
+
try:
|
|
48
|
+
return MysqlDB(**CONFIG.get(app=self.default_app(app), key="mysql").get(db))
|
|
49
|
+
except AttributeError as e:
|
|
50
|
+
traceback.print_exc()
|
|
51
|
+
pytest.exit(ExitCode.LOAD_DATABASE_INFO_ERROR)
|
|
52
|
+
|
|
53
|
+
def redis_conn(self, db, app=None):
|
|
54
|
+
try:
|
|
55
|
+
return RedisDB(**CONFIG.get(app=self.default_app(app), key="redis").get(db))
|
|
56
|
+
except AttributeError as e:
|
|
57
|
+
traceback.print_exc()
|
|
58
|
+
pytest.exit(ExitCode.LOAD_DATABASE_INFO_ERROR)
|
|
59
|
+
|
|
60
|
+
def default_app(self, app):
|
|
61
|
+
return app or self.belong_app
|
framework/conftest.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import copy
|
|
5
|
+
import inspect
|
|
6
|
+
import platform
|
|
7
|
+
import importlib
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urljoin
|
|
11
|
+
|
|
12
|
+
import retry
|
|
13
|
+
import allure
|
|
14
|
+
import pytest
|
|
15
|
+
from box import Box
|
|
16
|
+
from itertools import groupby
|
|
17
|
+
from framework.utils.log_util import logger
|
|
18
|
+
from framework.utils.yaml_util import YamlUtil
|
|
19
|
+
from framework.utils.encrypt import RsaByPubKey
|
|
20
|
+
from framework.utils.teams_util import TeamsUtil
|
|
21
|
+
from framework.utils.common import generate_2fa_code, snake_to_pascal, get_apps
|
|
22
|
+
from framework.exit_code import ExitCode
|
|
23
|
+
from framework.http_client import HttpClient
|
|
24
|
+
from framework.render_data import RenderData
|
|
25
|
+
from framework.allure_report import generate_report
|
|
26
|
+
from framework.global_attribute import CONTEXT, CONFIG
|
|
27
|
+
from config.settings import DATA_DIR, CASES_DIR, ROOT_DIR
|
|
28
|
+
|
|
29
|
+
test_results = {"total": 0, "passed": 0, "failed": 0, "skipped": 0}
|
|
30
|
+
# 获取当前系统的文件分隔符
|
|
31
|
+
file_separator = os.sep
|
|
32
|
+
|
|
33
|
+
# 判断用例是否需要被跳过
|
|
34
|
+
should_skip = False
|
|
35
|
+
all_app = get_apps()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pytest_configure(config):
|
|
39
|
+
"""
|
|
40
|
+
初始化时被调用,可以用于设置全局状态或配置
|
|
41
|
+
:param config:
|
|
42
|
+
:return:
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
for app in all_app:
|
|
46
|
+
# 将所有app对应环境的基础测试数据加到全局
|
|
47
|
+
CONTEXT.set_from_yaml(f"config/{app}/context.yaml", CONTEXT.env, app)
|
|
48
|
+
# 将所有app对应环境的中间件配置加到全局
|
|
49
|
+
CONFIG.set_from_yaml(f"config/{app}/config.yaml", CONTEXT.env, app)
|
|
50
|
+
CONTEXT.set(key="all_app", value=all_app)
|
|
51
|
+
CONTEXT.init_test_case_data_dict()
|
|
52
|
+
sys.path.append(CASES_DIR)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_data_path_by_case(app, case_file_name):
|
|
56
|
+
"""
|
|
57
|
+
基于case文件名称查找与之对应的yml文件路径
|
|
58
|
+
:param app:
|
|
59
|
+
:param case_file_name:
|
|
60
|
+
:return:
|
|
61
|
+
"""
|
|
62
|
+
for file_path in Path(os.path.join(DATA_DIR, app)).rglob(f"{case_file_name}.y*"):
|
|
63
|
+
if file_path:
|
|
64
|
+
return file_path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def __init_allure(params):
|
|
68
|
+
"""设置allure中case的 title, description, level"""
|
|
69
|
+
case_level_map = {
|
|
70
|
+
"p0": allure.severity_level.BLOCKER,
|
|
71
|
+
"p1": allure.severity_level.CRITICAL,
|
|
72
|
+
"p2": allure.severity_level.NORMAL,
|
|
73
|
+
"p3": allure.severity_level.MINOR,
|
|
74
|
+
"p4": allure.severity_level.TRIVIAL,
|
|
75
|
+
}
|
|
76
|
+
allure.dynamic.title(params.get("title"))
|
|
77
|
+
allure.dynamic.description(params.get("describe"))
|
|
78
|
+
allure.dynamic.severity(case_level_map.get(params.get("level")))
|
|
79
|
+
allure.dynamic.feature(params.get("module"))
|
|
80
|
+
allure.dynamic.story(params.get("describe"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pytest_generate_tests(metafunc):
|
|
84
|
+
"""
|
|
85
|
+
生成(多个)对测试函数的参数化调用
|
|
86
|
+
:param metafunc:
|
|
87
|
+
:return:
|
|
88
|
+
"""
|
|
89
|
+
# 获取测试函数所属的模块
|
|
90
|
+
module = metafunc.module
|
|
91
|
+
# 获取模块的文件路径
|
|
92
|
+
class_path = os.path.abspath(module.__file__)
|
|
93
|
+
root_path = ROOT_DIR + file_separator
|
|
94
|
+
class_id = class_path.replace(root_path, "").replace(file_separator, '.')
|
|
95
|
+
# 获取测试函数名
|
|
96
|
+
function_name = metafunc.function.__name__
|
|
97
|
+
# 构建全路径
|
|
98
|
+
full_id = f"{class_id}::{function_name}"
|
|
99
|
+
|
|
100
|
+
# print(f"测试用例全路径: {full_id}")
|
|
101
|
+
# 获取当前待执行用例的文件名
|
|
102
|
+
module_name = metafunc.module.__name__.split('.')[-1]
|
|
103
|
+
# 获取当前待执行用例的函数名
|
|
104
|
+
func_name = metafunc.function.__name__
|
|
105
|
+
# 获取测试用例所属app
|
|
106
|
+
belong_app = Path(class_path).relative_to(CASES_DIR).parts[0]
|
|
107
|
+
# 获取当前用例对应的测试数据路径
|
|
108
|
+
data_path = find_data_path_by_case(belong_app, module_name)
|
|
109
|
+
|
|
110
|
+
if not data_path:
|
|
111
|
+
logger.error(f"未找到{metafunc.module.__file__}对应的测试数据文件")
|
|
112
|
+
traceback.print_exc()
|
|
113
|
+
pytest.exit(ExitCode.CASE_YAML_NOT_EXIST)
|
|
114
|
+
test_data = YamlUtil(data_path).load_yml()
|
|
115
|
+
# 测试用例公共数据
|
|
116
|
+
case_common = test_data.get("case_common")
|
|
117
|
+
# 标记用例是否跳过
|
|
118
|
+
metafunc.function.ignore = case_common.get("ignore")
|
|
119
|
+
|
|
120
|
+
scenario_list = case_common.get('scenarios')
|
|
121
|
+
|
|
122
|
+
test_case_datas = []
|
|
123
|
+
|
|
124
|
+
if scenario_list is None:
|
|
125
|
+
test_case_datas = [{'casedata': {}}]
|
|
126
|
+
else:
|
|
127
|
+
for scenario in scenario_list:
|
|
128
|
+
if hasattr(CONTEXT, "mark"):
|
|
129
|
+
if scenario.get('scenario') is not None and str(scenario.get('scenario').get('flag')) == CONTEXT.mark:
|
|
130
|
+
test_case_datas.append({'casedata': scenario.get('scenario').get('data')})
|
|
131
|
+
else:
|
|
132
|
+
if scenario.get('scenario') is not None and scenario.get('scenario').get('data') is not None:
|
|
133
|
+
test_case_datas.append({'casedata': scenario.get('scenario').get('data')})
|
|
134
|
+
|
|
135
|
+
if len(scenario_list) == 0:
|
|
136
|
+
test_case_datas = [{'casedata': {}}]
|
|
137
|
+
|
|
138
|
+
case_data_list = []
|
|
139
|
+
ids = []
|
|
140
|
+
case_suite_index = 0
|
|
141
|
+
|
|
142
|
+
test_case_data_dict = dict()
|
|
143
|
+
|
|
144
|
+
# 是否是@test_suite_setup标识的setup方法
|
|
145
|
+
is_test_setup = 'test_setup' in metafunc.definition.keywords
|
|
146
|
+
is_test_teardown = 'test_teardown' in metafunc.definition.keywords
|
|
147
|
+
if is_test_setup or is_test_teardown:
|
|
148
|
+
for test_case_data in test_case_datas:
|
|
149
|
+
case_suite_index += 1
|
|
150
|
+
test_case_data_dict[f'{full_id}#{case_suite_index}'] = test_case_data
|
|
151
|
+
case_data_list.append(test_case_data)
|
|
152
|
+
ids.append(f'{full_id}#{str(case_suite_index)}')
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
|
|
156
|
+
for test_case_data in test_case_datas:
|
|
157
|
+
case_suite_index += 1
|
|
158
|
+
test_case_data_dict[f'{full_id}#{case_suite_index}'] = test_case_data
|
|
159
|
+
|
|
160
|
+
# 测试用例数据
|
|
161
|
+
case_data = test_data.get(func_name)
|
|
162
|
+
|
|
163
|
+
if not is_test_setup and not case_data:
|
|
164
|
+
logger.error(f"未找到用例{func_name}对应的数据")
|
|
165
|
+
pytest.exit(ExitCode.CASE_DATA_NOT_EXIST)
|
|
166
|
+
|
|
167
|
+
if case_data.get("request") is None:
|
|
168
|
+
case_data["request"] = dict()
|
|
169
|
+
if case_data.get("request").get("headers") is None:
|
|
170
|
+
case_data["request"]["headers"] = dict()
|
|
171
|
+
|
|
172
|
+
# 合并测试数据
|
|
173
|
+
case_data.setdefault("module", case_common.get("module"))
|
|
174
|
+
case_data.setdefault("describe", case_common.get("describe"))
|
|
175
|
+
case_data["_belong_app"] = belong_app
|
|
176
|
+
|
|
177
|
+
if case_data.get("request").get("url") is not None or case_common.get("url") is not None:
|
|
178
|
+
# 是一个带请求的case 反之 表示这条case不是一个带请求的case
|
|
179
|
+
domain = CONTEXT.get(key="domain", app=belong_app)
|
|
180
|
+
domain = domain if domain.startswith("http") else f"https://{domain}"
|
|
181
|
+
url = case_data.get("request").get("url")
|
|
182
|
+
if url.startswith('${'):
|
|
183
|
+
placeholder = url[2:len(url) - 1]
|
|
184
|
+
actual_url = CONTEXT.get(key=placeholder, app=belong_app)
|
|
185
|
+
if actual_url:
|
|
186
|
+
url = actual_url
|
|
187
|
+
method = case_data.get("request").get("method")
|
|
188
|
+
if not url:
|
|
189
|
+
if not case_common.get("url"):
|
|
190
|
+
logger.error(f"测试数据request中缺少必填字段: url", case_data)
|
|
191
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
192
|
+
case_data["request"]["url"] = urljoin(domain, case_common.get("url"))
|
|
193
|
+
else:
|
|
194
|
+
case_data["request"]["url"] = urljoin(domain, url)
|
|
195
|
+
|
|
196
|
+
if not method:
|
|
197
|
+
if not case_common.get("method"):
|
|
198
|
+
logger.error(f"测试数据request中缺少必填字段: method", case_data)
|
|
199
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
200
|
+
case_data["request"]["method"] = case_common.get("method")
|
|
201
|
+
|
|
202
|
+
for key in ["title"]:
|
|
203
|
+
if key not in case_data:
|
|
204
|
+
logger.error(f"测试数据{func_name}中缺少必填字段: {key}", case_data)
|
|
205
|
+
pytest.exit(ExitCode.YAML_MISSING_FIELDS)
|
|
206
|
+
|
|
207
|
+
# 给用例打order标记
|
|
208
|
+
if case_data.get("order", None):
|
|
209
|
+
metafunc.function.order = int(case_data.get("order"))
|
|
210
|
+
else:
|
|
211
|
+
metafunc.function.order = None
|
|
212
|
+
|
|
213
|
+
# 给用例打mark标记
|
|
214
|
+
if case_data.get("mark", None):
|
|
215
|
+
metafunc.function.marks = [case_data.get("mark"), case_data.get("level")]
|
|
216
|
+
else:
|
|
217
|
+
metafunc.function.marks = [case_data.get("level")]
|
|
218
|
+
|
|
219
|
+
case_data_list.append(case_data)
|
|
220
|
+
ids.append(f'{full_id}#{case_suite_index}')
|
|
221
|
+
|
|
222
|
+
CONTEXT.set(key=f'{full_id}#test_case_datas', value=test_case_data_dict)
|
|
223
|
+
|
|
224
|
+
metafunc.parametrize("data", case_data_list, ids=ids, scope='function')
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.hookimpl
|
|
228
|
+
def pytest_runtest_setup(item):
|
|
229
|
+
allure.dynamic.sub_suite(item.allure_suite_mark)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def pytest_collection_modifyitems(items):
|
|
233
|
+
# 重新排序
|
|
234
|
+
new_items = sort(items)
|
|
235
|
+
# Demo: new_items = [items[0],items[2],items[1],items[3]]
|
|
236
|
+
items[:] = new_items
|
|
237
|
+
for item in items:
|
|
238
|
+
# 用例打标记
|
|
239
|
+
try:
|
|
240
|
+
marks = item.function.marks
|
|
241
|
+
for mark in marks:
|
|
242
|
+
item.add_marker(mark)
|
|
243
|
+
# 用例排序
|
|
244
|
+
order = item.function.order
|
|
245
|
+
if order:
|
|
246
|
+
item.add_marker(pytest.mark.order(order))
|
|
247
|
+
except Exception: # 忽略test_setup,test_teardown方法
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def __get_group_key__(item):
|
|
252
|
+
return '::'.join(item.nodeid.split('::')[:2])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def sort(case_items):
|
|
256
|
+
# 按测试类全路径分类,同一个类文件的用例归集到一起
|
|
257
|
+
# 使用 groupby 函数进行分组
|
|
258
|
+
item_group_list = [list(group) for _, group in groupby(case_items, key=__get_group_key__)]
|
|
259
|
+
|
|
260
|
+
all_item_list = []
|
|
261
|
+
clase_id = None
|
|
262
|
+
for items in item_group_list:
|
|
263
|
+
# 找出被test_setup标记的方法
|
|
264
|
+
custom_scope_setup_items = [item for item in items if 'test_setup' in item.keywords]
|
|
265
|
+
custom_scope_teardown_items = [item for item in items if 'test_teardown' in item.keywords]
|
|
266
|
+
# 未被test_setup/test_teardown 标记的test方法
|
|
267
|
+
non_custom_scope_items = [item for item in items if
|
|
268
|
+
'test_setup' not in item.keywords and 'test_teardown' not in item.keywords]
|
|
269
|
+
item_list = []
|
|
270
|
+
# 用例的组数
|
|
271
|
+
case_suite_num = 0
|
|
272
|
+
# 生成每个组当前的索引
|
|
273
|
+
ori_name_temp = None
|
|
274
|
+
ori_name_list = []
|
|
275
|
+
|
|
276
|
+
for item in non_custom_scope_items:
|
|
277
|
+
clase_id = item.cls.__name__
|
|
278
|
+
original_name = item.originalname
|
|
279
|
+
|
|
280
|
+
if ori_name_temp is None or ori_name_temp == original_name:
|
|
281
|
+
ori_name_temp = original_name
|
|
282
|
+
case_suite_num += 1
|
|
283
|
+
ori_name_list.append([original_name, item])
|
|
284
|
+
else:
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
# 根据组数 创建各组的数组 并插入第一个case
|
|
288
|
+
case_dict = dict()
|
|
289
|
+
for i in range(case_suite_num):
|
|
290
|
+
item = ori_name_list[i][1]
|
|
291
|
+
id = item.callspec.id
|
|
292
|
+
|
|
293
|
+
first_part = id.split('#', 1)[-1]
|
|
294
|
+
index = first_part.split(']')[0]
|
|
295
|
+
case_dict[index] = [item]
|
|
296
|
+
|
|
297
|
+
new_start_index = case_suite_num
|
|
298
|
+
# 以new_start_index为起点 重新遍历items
|
|
299
|
+
for i in range(new_start_index, len(non_custom_scope_items)):
|
|
300
|
+
item = non_custom_scope_items[i]
|
|
301
|
+
id = item.callspec.id
|
|
302
|
+
first_part = id.split('#', 1)[-1]
|
|
303
|
+
index = first_part.split(']')[0]
|
|
304
|
+
case_dict.get(index).append(item)
|
|
305
|
+
|
|
306
|
+
setup_dict = dict()
|
|
307
|
+
for item in custom_scope_setup_items:
|
|
308
|
+
id = item.callspec.id
|
|
309
|
+
first_part = id.split('#', 1)[-1]
|
|
310
|
+
index = first_part.split(']')[0]
|
|
311
|
+
setup_dict[index] = [item]
|
|
312
|
+
|
|
313
|
+
teardown_dict = dict()
|
|
314
|
+
for item in custom_scope_teardown_items:
|
|
315
|
+
id = item.callspec.id
|
|
316
|
+
first_part = id.split('#', 1)[-1]
|
|
317
|
+
index = first_part.split(']')[0]
|
|
318
|
+
teardown_dict[index] = [item]
|
|
319
|
+
|
|
320
|
+
index = 0
|
|
321
|
+
for id in case_dict:
|
|
322
|
+
index += 1
|
|
323
|
+
if setup_dict:
|
|
324
|
+
setup_item_list = setup_dict.get(id)
|
|
325
|
+
for item in setup_item_list:
|
|
326
|
+
allure_suite_mark = f'{clase_id}#{index}'
|
|
327
|
+
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
328
|
+
|
|
329
|
+
item_list += setup_item_list
|
|
330
|
+
|
|
331
|
+
case_item_list = case_dict.get(id)
|
|
332
|
+
for item in case_item_list:
|
|
333
|
+
allure_suite_mark = f'{clase_id}#{index}'
|
|
334
|
+
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
335
|
+
item_list += case_item_list
|
|
336
|
+
|
|
337
|
+
if teardown_dict:
|
|
338
|
+
teardown_item_list = teardown_dict.get(id)
|
|
339
|
+
for item in teardown_item_list:
|
|
340
|
+
allure_suite_mark = f'{clase_id}#{index}'
|
|
341
|
+
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
342
|
+
item_list += teardown_item_list
|
|
343
|
+
|
|
344
|
+
all_item_list += item_list
|
|
345
|
+
|
|
346
|
+
return all_item_list
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def pytest_runtest_call(item):
|
|
350
|
+
"""
|
|
351
|
+
模版渲染,运行用例
|
|
352
|
+
:param item:
|
|
353
|
+
:return:
|
|
354
|
+
"""
|
|
355
|
+
# 是否是test—setup 或 test-teardown方法
|
|
356
|
+
is_around_function = item.get_closest_marker("test_setup") or item.get_closest_marker("test_teardown")
|
|
357
|
+
if not is_around_function:
|
|
358
|
+
if item.function.ignore:
|
|
359
|
+
# 如果是普通方法 而且由于之前步骤出错则skiped
|
|
360
|
+
pytest.skip('skiped')
|
|
361
|
+
|
|
362
|
+
id = item.callspec.id
|
|
363
|
+
|
|
364
|
+
# 支持本地调试的标记
|
|
365
|
+
CONTEXT.set(key='run#index', value=id.split('::')[1])
|
|
366
|
+
|
|
367
|
+
# 获取参数化数据 放入上下文中
|
|
368
|
+
full_id = id.split('#')[0]
|
|
369
|
+
test_case_datas = CONTEXT.get(key=f'{full_id}#test_case_datas')
|
|
370
|
+
CONTEXT.set_from_dict(test_case_datas.get(id))
|
|
371
|
+
|
|
372
|
+
# 获取原始测试数据
|
|
373
|
+
origin_data = item.funcargs.get("data")
|
|
374
|
+
# 深拷贝这份数据
|
|
375
|
+
deep_copied_origin_data = copy.deepcopy(origin_data)
|
|
376
|
+
item.funcargs["origin_data"] = Box(origin_data)
|
|
377
|
+
|
|
378
|
+
__init_allure(origin_data)
|
|
379
|
+
logger.info(f"执行用例: {item.nodeid}")
|
|
380
|
+
# 对原始请求数据进行渲染替换
|
|
381
|
+
rendered_data = RenderData(deep_copied_origin_data).render()
|
|
382
|
+
# 测试用例函数添加参数data
|
|
383
|
+
http = item.funcargs.get("http")
|
|
384
|
+
item.funcargs["data"] = Box(rendered_data)
|
|
385
|
+
item.funcargs["belong_app"] = origin_data.get("_belong_app")
|
|
386
|
+
item.funcargs["config"] = CONFIG
|
|
387
|
+
item.funcargs["context"] = CONTEXT
|
|
388
|
+
if item.cls:
|
|
389
|
+
item.cls.http = http
|
|
390
|
+
item.cls.data = Box(rendered_data)
|
|
391
|
+
item.cls.origin_data = Box(origin_data)
|
|
392
|
+
item.cls.belong_app = origin_data.get("_belong_app")
|
|
393
|
+
item.cls.context = CONTEXT
|
|
394
|
+
item.cls.config = config
|
|
395
|
+
|
|
396
|
+
# 获取测试函数体内容
|
|
397
|
+
func_source = re.sub(r'(?<!["\'])#.*', '', inspect.getsource(item.function))
|
|
398
|
+
# 校验测试用例中是否有断言
|
|
399
|
+
if 'test_setup' not in item.keywords and 'test_teardown' not in item.keywords and "assert" not in func_source:
|
|
400
|
+
logger.error(f"测试方法:{item.originalname}缺少断言")
|
|
401
|
+
pytest.exit(ExitCode.MISSING_ASSERTIONS)
|
|
402
|
+
|
|
403
|
+
# # 检查函数体内容是否包含语句self.request,如果没有则自动发送请求
|
|
404
|
+
# if "self.request" not in func_source:
|
|
405
|
+
# # 测试用例函数添加参数response
|
|
406
|
+
# response = client.request(rendered_data)
|
|
407
|
+
# item.funcargs["response"] = response
|
|
408
|
+
# if item.cls:
|
|
409
|
+
# item.cls.response = response
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def pytest_runtest_logreport(report):
|
|
413
|
+
"""收集测试结果"""
|
|
414
|
+
if report.when == "call": # 确保是测试调用阶段(忽略setup和teardown)
|
|
415
|
+
test_results["total"] += 1
|
|
416
|
+
if report.passed:
|
|
417
|
+
test_results["passed"] += 1
|
|
418
|
+
elif report.failed:
|
|
419
|
+
test_results["failed"] += 1
|
|
420
|
+
elif report.skipped:
|
|
421
|
+
test_results["skipped"] += 1
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
425
|
+
def pytest_runtest_makereport(item, call):
|
|
426
|
+
# 获取测试报告
|
|
427
|
+
outcome = yield
|
|
428
|
+
report = outcome.get_result()
|
|
429
|
+
|
|
430
|
+
# 如果测试失败并且是执行阶段
|
|
431
|
+
if report.when == "call" and report.failed:
|
|
432
|
+
# 获取断言失败的消息
|
|
433
|
+
logger.error(f"断言失败: {report.longrepr.reprcrash}")
|
|
434
|
+
# 用于控制失败步骤后是否继续的标志
|
|
435
|
+
failStepContinuationFlag = item.callspec.params.get('data').get('failStepContinuationFlag')
|
|
436
|
+
|
|
437
|
+
global should_skip
|
|
438
|
+
if failStepContinuationFlag is None:
|
|
439
|
+
should_skip = True
|
|
440
|
+
else:
|
|
441
|
+
if failStepContinuationFlag == True:
|
|
442
|
+
should_skip = False
|
|
443
|
+
else:
|
|
444
|
+
should_skip = True
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
448
|
+
"""在 pytest 结束后修改统计数据或添加自定义报告"""
|
|
449
|
+
stats = terminalreporter.stats
|
|
450
|
+
# 统计各种测试结果
|
|
451
|
+
passed = len(stats.get("passed", []))
|
|
452
|
+
failed = len(stats.get("failed", []))
|
|
453
|
+
skipped = len(stats.get("skipped", []))
|
|
454
|
+
total = passed + failed + skipped
|
|
455
|
+
try:
|
|
456
|
+
pass_rate = round(passed / (total - skipped) * 100, 2)
|
|
457
|
+
except ZeroDivisionError:
|
|
458
|
+
pass_rate = 0
|
|
459
|
+
# 打印自定义统计信息
|
|
460
|
+
terminalreporter.write("\n============ 执行结果统计 ============\n", blue=True, bold=True)
|
|
461
|
+
terminalreporter.write(f"执行用例总数: {passed + failed + skipped}\n", bold=True)
|
|
462
|
+
terminalreporter.write(f"通过用例数: {passed}\n", green=True, bold=True)
|
|
463
|
+
terminalreporter.write(f"失败用例数: {failed}\n", red=True, bold=True)
|
|
464
|
+
terminalreporter.write(f"跳过用例数: {skipped}\n", yellow=True, bold=True)
|
|
465
|
+
terminalreporter.write(f"用例通过率: {pass_rate}%\n", green=True, bold=True)
|
|
466
|
+
terminalreporter.write("====================================\n", blue=True, bold=True)
|
|
467
|
+
# 生成allure测试报告
|
|
468
|
+
generate_report()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@pytest.fixture(autouse=True)
|
|
472
|
+
def response():
|
|
473
|
+
response = None
|
|
474
|
+
yield response
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@pytest.fixture(autouse=True)
|
|
478
|
+
def data():
|
|
479
|
+
data: dict = dict()
|
|
480
|
+
yield data
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@pytest.fixture(autouse=True)
|
|
484
|
+
def origin_data():
|
|
485
|
+
origin_data: dict = dict()
|
|
486
|
+
yield origin_data
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@pytest.fixture(autouse=True)
|
|
490
|
+
def belong_app():
|
|
491
|
+
app = None
|
|
492
|
+
yield app
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@pytest.fixture(autouse=True)
|
|
496
|
+
def config():
|
|
497
|
+
config = None
|
|
498
|
+
yield config
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.fixture(autouse=True)
|
|
502
|
+
def context():
|
|
503
|
+
context = None
|
|
504
|
+
yield context
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class Http(object):
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@retry.retry(tries=3, delay=1)
|
|
512
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
513
|
+
def http():
|
|
514
|
+
module = importlib.import_module("conftest")
|
|
515
|
+
try:
|
|
516
|
+
for app in all_app:
|
|
517
|
+
setattr(Http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
|
|
518
|
+
return Http
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logger.error(f"登录{app}异常:{e}")
|
|
521
|
+
traceback.print_exc()
|
|
522
|
+
pytest.exit(ExitCode.LOGIN_ERROR)
|
|
523
|
+
return None
|
framework/db/__init__.py
ADDED
|
File without changes
|