pytest-api-framework-alpha 0.1.0__tar.gz

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.
Files changed (29) hide show
  1. pytest_api_framework_alpha-0.1.0/PKG-INFO +29 -0
  2. pytest_api_framework_alpha-0.1.0/framework/__init__.py +0 -0
  3. pytest_api_framework_alpha-0.1.0/framework/allure_report.py +35 -0
  4. pytest_api_framework_alpha-0.1.0/framework/base_class.py +61 -0
  5. pytest_api_framework_alpha-0.1.0/framework/conftest.py +523 -0
  6. pytest_api_framework_alpha-0.1.0/framework/db/__init__.py +0 -0
  7. pytest_api_framework_alpha-0.1.0/framework/db/mysql_db.py +111 -0
  8. pytest_api_framework_alpha-0.1.0/framework/db/redis_db.py +142 -0
  9. pytest_api_framework_alpha-0.1.0/framework/exit_code.py +19 -0
  10. pytest_api_framework_alpha-0.1.0/framework/extract.py +101 -0
  11. pytest_api_framework_alpha-0.1.0/framework/global_attribute.py +100 -0
  12. pytest_api_framework_alpha-0.1.0/framework/http_client.py +163 -0
  13. pytest_api_framework_alpha-0.1.0/framework/render_data.py +185 -0
  14. pytest_api_framework_alpha-0.1.0/framework/report.py +102 -0
  15. pytest_api_framework_alpha-0.1.0/framework/startapp.py +126 -0
  16. pytest_api_framework_alpha-0.1.0/framework/utils/__init__.py +0 -0
  17. pytest_api_framework_alpha-0.1.0/framework/utils/common.py +211 -0
  18. pytest_api_framework_alpha-0.1.0/framework/utils/encrypt.py +387 -0
  19. pytest_api_framework_alpha-0.1.0/framework/utils/log_util.py +3 -0
  20. pytest_api_framework_alpha-0.1.0/framework/utils/teams_util.py +48 -0
  21. pytest_api_framework_alpha-0.1.0/framework/utils/yaml_util.py +25 -0
  22. pytest_api_framework_alpha-0.1.0/framework/validate.py +207 -0
  23. pytest_api_framework_alpha-0.1.0/pytest_api_framework_alpha.egg-info/PKG-INFO +29 -0
  24. pytest_api_framework_alpha-0.1.0/pytest_api_framework_alpha.egg-info/SOURCES.txt +27 -0
  25. pytest_api_framework_alpha-0.1.0/pytest_api_framework_alpha.egg-info/dependency_links.txt +1 -0
  26. pytest_api_framework_alpha-0.1.0/pytest_api_framework_alpha.egg-info/requires.txt +20 -0
  27. pytest_api_framework_alpha-0.1.0/pytest_api_framework_alpha.egg-info/top_level.txt +1 -0
  28. pytest_api_framework_alpha-0.1.0/setup.cfg +4 -0
  29. pytest_api_framework_alpha-0.1.0/setup.py +34 -0
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-api-framework-alpha
3
+ Version: 0.1.0
4
+ Author: alpha
5
+ Author-email:
6
+ Requires-Python: >=3.6
7
+ Requires-Dist: allure-pytest==2.13.1
8
+ Requires-Dist: allure-python-commons==2.13.1
9
+ Requires-Dist: cn2an==0.5.19
10
+ Requires-Dist: DBUtils==3.1.0
11
+ Requires-Dist: Faker==18.3.2
12
+ Requires-Dist: jsonpath==0.82
13
+ Requires-Dist: pytest==7.2.2
14
+ Requires-Dist: python-dotenv==1.0.1
15
+ Requires-Dist: PyYAML==6.0.1
16
+ Requires-Dist: python-box==7.2.0
17
+ Requires-Dist: pycryptodome==3.21.0
18
+ Requires-Dist: pyotp==2.9.0
19
+ Requires-Dist: pytest-order==1.3.0
20
+ Requires-Dist: PyMySQL==1.1.0
21
+ Requires-Dist: redis==3.5.3
22
+ Requires-Dist: requests==2.25.1
23
+ Requires-Dist: requests-toolbelt==1.0.0
24
+ Requires-Dist: retry==0.9.2
25
+ Requires-Dist: pytest-rerunfailures==11.1
26
+ Requires-Dist: pytest-timeout==2.2.0
27
+ Dynamic: author
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
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)
@@ -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
@@ -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