pytest-api-framework-alpha 0.1.4__tar.gz → 0.1.6__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.
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/PKG-INFO +1 -3
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/base_class.py +22 -15
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/conftest.py +57 -30
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/extract.py +11 -6
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/http_client.py +15 -10
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/startapp.py +7 -4
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/common.py +19 -5
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/validate.py +43 -19
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/PKG-INFO +1 -3
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/requires.txt +0 -2
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/setup.py +1 -3
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/allure_report.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/mysql_db.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/redis_db.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/exit_code.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/global_attribute.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/render_data.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/report.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/__init__.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/encrypt.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/log_util.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/teams_util.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/yaml_util.py +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/SOURCES.txt +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
- {pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-api-framework-alpha
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Author: alpha
|
|
5
5
|
Author-email:
|
|
6
6
|
Requires-Python: >=3.6
|
|
@@ -22,8 +22,6 @@ Requires-Dist: redis==3.5.3
|
|
|
22
22
|
Requires-Dist: requests==2.25.1
|
|
23
23
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
24
24
|
Requires-Dist: retry==0.9.2
|
|
25
|
-
Requires-Dist: pytest-rerunfailures==11.1
|
|
26
|
-
Requires-Dist: pytest-timeout==2.2.0
|
|
27
25
|
Requires-Dist: dill==0.3.8
|
|
28
26
|
Dynamic: author
|
|
29
27
|
Dynamic: requires-dist
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/base_class.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import traceback
|
|
2
|
+
import importlib
|
|
2
3
|
from urllib.parse import urlparse, urlunparse, urljoin
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -9,17 +10,20 @@ from framework.db.mysql_db import MysqlDB
|
|
|
9
10
|
from framework.db.redis_db import RedisDB
|
|
10
11
|
from framework.utils.log_util import logger
|
|
11
12
|
from framework.http_client import ResponseUtil
|
|
12
|
-
from framework.
|
|
13
|
+
from framework.utils.common import snake_to_pascal
|
|
14
|
+
from framework.global_attribute import CONFIG, GlobalAttribute, _FRAMEWORK_CONTEXT
|
|
15
|
+
from config.settings import UNAUTHORIZED_CODE
|
|
16
|
+
module = importlib.import_module("test_case.conftest")
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class BaseTestCase(object):
|
|
16
|
-
context: GlobalAttribute = None
|
|
17
|
-
config: GlobalAttribute = None
|
|
18
20
|
http = None
|
|
19
21
|
data: Box = None
|
|
20
22
|
scenario: Box = None
|
|
21
23
|
belong_app = None
|
|
22
24
|
response: ResponseUtil = None
|
|
25
|
+
context: GlobalAttribute = None
|
|
26
|
+
config: GlobalAttribute = None
|
|
23
27
|
|
|
24
28
|
def request(self, app=None, *, account, data, **kwargs):
|
|
25
29
|
try:
|
|
@@ -28,6 +32,9 @@ class BaseTestCase(object):
|
|
|
28
32
|
domain = self.context.get(app).get("domain")
|
|
29
33
|
data.request.url = self.replace_domain(data.request.url, domain)
|
|
30
34
|
self.response = getattr(app_http, account).request(data=data, kwargs=kwargs)
|
|
35
|
+
if self.response.status_code in UNAUTHORIZED_CODE:
|
|
36
|
+
_http = _FRAMEWORK_CONTEXT.get(key="_http")
|
|
37
|
+
setattr(_http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
|
|
31
38
|
return self.response
|
|
32
39
|
except AttributeError as e:
|
|
33
40
|
logger.error(f"app {app} or account {account} no exist: {e}")
|
|
@@ -37,27 +44,27 @@ class BaseTestCase(object):
|
|
|
37
44
|
|
|
38
45
|
def post(self, app, account, url, data=None, json=None, **kwargs):
|
|
39
46
|
domain = self.context.get(app).get("domain")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return self.
|
|
47
|
+
request = {"url": urljoin(domain, url), "data": data, "json": json}
|
|
48
|
+
request.update({"method": "post", "headers": {}, **kwargs})
|
|
49
|
+
return self.request(app=app, account=account, data=Box({"request": request}))
|
|
43
50
|
|
|
44
51
|
def get(self, app, account, url, params=None, **kwargs):
|
|
45
52
|
domain = self.context.get(app).get("domain")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return self.
|
|
53
|
+
request = {"url": urljoin(domain, url), "params": params}
|
|
54
|
+
request.update({"method": "get", "headers": {}, **kwargs})
|
|
55
|
+
return self.request(app=app, account=account, data=Box({"request": request}))
|
|
49
56
|
|
|
50
57
|
def put(self, app, account, url, data=None, **kwargs):
|
|
51
58
|
domain = self.context.get(app).get("domain")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return self.
|
|
59
|
+
request = {"url": urljoin(domain, url), "data": data}
|
|
60
|
+
request.update({"method": "put", "headers": {}, **kwargs})
|
|
61
|
+
return self.request(app=app, account=account, data=Box({"request": request}))
|
|
55
62
|
|
|
56
63
|
def delete(self, app, account, url, **kwargs):
|
|
57
64
|
domain = self.context.get(app).get("domain")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return self.
|
|
65
|
+
request = {"url": urljoin(domain, url)}
|
|
66
|
+
request.update({"method": "delete", "headers": {}, **kwargs})
|
|
67
|
+
return self.request(app=app, account=account, data=Box({"request": request}))
|
|
61
68
|
|
|
62
69
|
def mysql_conn(self, db, app=None):
|
|
63
70
|
try:
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from itertools import chain
|
|
9
9
|
from urllib.parse import urljoin
|
|
10
10
|
from collections import OrderedDict
|
|
11
|
+
from datetime import datetime, timedelta
|
|
11
12
|
|
|
12
13
|
import dill
|
|
13
14
|
import retry
|
|
@@ -25,6 +26,7 @@ from framework.utils.common import snake_to_pascal, get_apps, convert_numbers_to
|
|
|
25
26
|
from config.settings import DATA_DIR, CASES_DIR
|
|
26
27
|
|
|
27
28
|
all_app = get_apps()
|
|
29
|
+
module = importlib.import_module("test_case.conftest")
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def find_data_path_by_case(app, case_file_name):
|
|
@@ -100,6 +102,8 @@ def pytest_generate_tests(metafunc):
|
|
|
100
102
|
test_data = YamlUtil(data_path).load_yml()
|
|
101
103
|
# 测试用例公共数据
|
|
102
104
|
case_common = test_data.get("case_common")
|
|
105
|
+
if case_common.get("ignore"):
|
|
106
|
+
pytest.skip("Skipp the case because ignore")
|
|
103
107
|
scenarios = case_common.get("scenarios")
|
|
104
108
|
# 测试用例数据
|
|
105
109
|
case_data = test_data.get(func_name)
|
|
@@ -155,25 +159,26 @@ def pytest_generate_tests(metafunc):
|
|
|
155
159
|
if scenarios:
|
|
156
160
|
ids = list()
|
|
157
161
|
for index, item in enumerate(scenarios):
|
|
158
|
-
if
|
|
162
|
+
if item.get("scenario").get("ignore"):
|
|
163
|
+
continue
|
|
164
|
+
if func_name in item.get("scenario").get("exclude", list()):
|
|
159
165
|
continue
|
|
160
166
|
_mark = CONTEXT.get("mark")
|
|
161
|
-
if _mark:
|
|
162
|
-
|
|
163
|
-
if flag != _mark:
|
|
164
|
-
continue
|
|
167
|
+
if _mark and item.get("scenario").get("flag") != _mark:
|
|
168
|
+
continue
|
|
165
169
|
deep_copied_case_data = copy.deepcopy(case_data)
|
|
166
170
|
try:
|
|
167
171
|
deep_copied_case_data["_scenario"] = item.get("scenario")
|
|
168
172
|
case_data_list.append(deep_copied_case_data)
|
|
169
173
|
ids.append(case_data.get("title") + f"#{index + 1}")
|
|
170
174
|
except KeyError as e:
|
|
171
|
-
logger.error(f"
|
|
175
|
+
logger.error(f"scenario参数化格式不正确:{e}")
|
|
172
176
|
traceback.print_exc()
|
|
173
177
|
pytest.exit(ExitCode.PARAMETRIZE_ATTRIBUTE_NOT_EXIT)
|
|
174
178
|
metafunc.parametrize("data", case_data_list, ids=ids, scope="function")
|
|
175
179
|
else:
|
|
176
180
|
if not case_common.get("ignore"):
|
|
181
|
+
case_data["_scenario"] = {"data": {}}
|
|
177
182
|
case_data_list = [case_data]
|
|
178
183
|
# 进行参数化生成用例
|
|
179
184
|
metafunc.parametrize("data", case_data_list, ids=[f'{case_data.get("title")}#1'], scope="function")
|
|
@@ -277,11 +282,13 @@ def pytest_runtest_call(item):
|
|
|
277
282
|
for prev_item in reversed(item.session.items[:index]): # 只遍历当前 item 之前的
|
|
278
283
|
if prev_item.parent.name == current_cls_name and re.search(pattern, prev_item.name).group(
|
|
279
284
|
1) == current_turn.group(1): # 确保是同一个类
|
|
280
|
-
status = getattr(prev_item, "status", None) # 访问
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
status = getattr(prev_item, "status", None) # 访问 status 属性
|
|
286
|
+
skip_reason = getattr(prev_item, "skip_reason", None) # 访问 skip_reason 属性
|
|
287
|
+
if status == "skipped" and skip_reason in ["the previous method execution skipped",
|
|
288
|
+
"the previous method execution failed"]:
|
|
289
|
+
pytest.skip("the previous method execution skipped")
|
|
283
290
|
elif status == "failed":
|
|
284
|
-
pytest.skip(
|
|
291
|
+
pytest.skip("the previous method execution failed")
|
|
285
292
|
|
|
286
293
|
# 获取原始测试数据
|
|
287
294
|
origin_data = item.funcargs.get("data")
|
|
@@ -291,25 +298,29 @@ def pytest_runtest_call(item):
|
|
|
291
298
|
rendered_data = RenderData(origin_data).render()
|
|
292
299
|
# 函数式测试用例添加参数data, belong_app
|
|
293
300
|
http = item.funcargs.get("http")
|
|
294
|
-
item.funcargs["data"] = Box(rendered_data)
|
|
295
|
-
item.funcargs["scenario"] =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
item.funcargs["
|
|
301
|
+
item.funcargs["data"] = item.instance.data = Box(rendered_data)
|
|
302
|
+
item.funcargs["scenario"] = item.instance.scenario = Box(
|
|
303
|
+
convert_numbers_to_decimal(rendered_data.get("_scenario").get("data")))
|
|
304
|
+
_belong_app = origin_data.get("_belong_app")
|
|
305
|
+
item.funcargs["belong_app"] = item.instance.belong_app = _belong_app
|
|
306
|
+
item.funcargs["config"] = item.instance.config = CONFIG
|
|
307
|
+
item.funcargs["context"] = item.instance.context = CONTEXT
|
|
299
308
|
# 类式测试用例添加参数http,data, belong_app
|
|
300
309
|
item.instance.http = http
|
|
301
|
-
item.instance.data = Box(rendered_data)
|
|
302
|
-
item.instance.scenario = Box(convert_numbers_to_decimal(rendered_data.get("_scenario").get("data")))
|
|
303
|
-
item.instance.belong_app = origin_data.get("_belong_app")
|
|
304
|
-
item.instance.context = CONTEXT
|
|
305
|
-
item.instance.config = CONFIG
|
|
306
310
|
|
|
307
|
-
# 获取测试函数体内容
|
|
308
|
-
func_source = re.sub(r'(?<!["\'])#.*', '', dill.source.getsource(item.function))
|
|
309
|
-
# 校验测试用例中是否有断言
|
|
310
|
-
if "assert" not in func_source:
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
# # 获取测试函数体内容
|
|
312
|
+
# func_source = re.sub(r'(?<!["\'])#.*', '', dill.source.getsource(item.function))
|
|
313
|
+
# # 校验测试用例中是否有断言
|
|
314
|
+
# if "assert" not in func_source:
|
|
315
|
+
# logger.error(f"测试方法:{item.originalname}缺少断言")
|
|
316
|
+
# pytest.exit(ExitCode.MISSING_ASSERTIONS)
|
|
317
|
+
|
|
318
|
+
# 判断token是否过期,过期则重新登录
|
|
319
|
+
expire_time = _FRAMEWORK_CONTEXT.get(_belong_app)
|
|
320
|
+
if expire_time:
|
|
321
|
+
_http = _FRAMEWORK_CONTEXT.get("_http")
|
|
322
|
+
if datetime.now() >= expire_time:
|
|
323
|
+
setattr(_http, _belong_app, getattr(module, f"{snake_to_pascal(_belong_app)}Login")(_belong_app))
|
|
313
324
|
|
|
314
325
|
|
|
315
326
|
def pytest_runtest_teardown(item):
|
|
@@ -330,6 +341,17 @@ def pytest_runtest_makereport(item, call):
|
|
|
330
341
|
report = outcome.get_result()
|
|
331
342
|
# 将测试结果存储到 item 对象的自定义属性 `_test_status`
|
|
332
343
|
if report.when == "call": # 只记录测试执行阶段的状态,不包括 setup/teardown
|
|
344
|
+
longrepr = report.longrepr
|
|
345
|
+
if longrepr:
|
|
346
|
+
try:
|
|
347
|
+
if ":" in longrepr[2]:
|
|
348
|
+
key, reason = longrepr[2].split(":")
|
|
349
|
+
else:
|
|
350
|
+
key, reason = longrepr[2], ""
|
|
351
|
+
if key == "Skipped":
|
|
352
|
+
item.skip_reason = reason
|
|
353
|
+
except:
|
|
354
|
+
pass
|
|
333
355
|
item.status = report.outcome # 'passed', 'failed', or 'skipped'
|
|
334
356
|
|
|
335
357
|
|
|
@@ -347,7 +369,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
|
347
369
|
pass_rate = 0
|
|
348
370
|
# 打印自定义统计信息
|
|
349
371
|
terminalreporter.write("\n============ 执行结果统计 ============\n", blue=True, bold=True)
|
|
350
|
-
terminalreporter.write(f"执行用例总数: {passed + failed
|
|
372
|
+
terminalreporter.write(f"执行用例总数: {passed + failed}\n", bold=True)
|
|
351
373
|
terminalreporter.write(f"通过用例数: {passed}\n", green=True, bold=True)
|
|
352
374
|
terminalreporter.write(f"失败用例数: {failed}\n", red=True, bold=True)
|
|
353
375
|
terminalreporter.write(f"跳过用例数: {skipped}\n", yellow=True, bold=True)
|
|
@@ -400,9 +422,9 @@ def context():
|
|
|
400
422
|
|
|
401
423
|
|
|
402
424
|
@retry.retry(tries=3, delay=1)
|
|
403
|
-
@pytest.fixture(scope="
|
|
425
|
+
@pytest.fixture(scope="function", autouse=True)
|
|
404
426
|
def http():
|
|
405
|
-
yield
|
|
427
|
+
yield _FRAMEWORK_CONTEXT.get("_http")
|
|
406
428
|
|
|
407
429
|
|
|
408
430
|
class Http(object):
|
|
@@ -410,10 +432,15 @@ class Http(object):
|
|
|
410
432
|
|
|
411
433
|
|
|
412
434
|
def login():
|
|
413
|
-
module = importlib.import_module("conftest")
|
|
414
435
|
try:
|
|
436
|
+
logger.info("登录账号".center(80, "*"))
|
|
415
437
|
for app in all_app:
|
|
416
438
|
setattr(Http, app, getattr(module, f"{snake_to_pascal(app)}Login")(app))
|
|
439
|
+
token_expiry = CONTEXT.get(app).get("token_expiry")
|
|
440
|
+
if token_expiry:
|
|
441
|
+
expire_time = datetime.now() + timedelta(seconds=token_expiry)
|
|
442
|
+
_FRAMEWORK_CONTEXT.set(key=app, value=expire_time)
|
|
443
|
+
logger.info("登录完成".center(80, "*"))
|
|
417
444
|
return Http
|
|
418
445
|
except Exception as e:
|
|
419
446
|
logger.error(f"登录{app}异常:{e}")
|
|
@@ -4,7 +4,7 @@ from typing import List
|
|
|
4
4
|
|
|
5
5
|
import allure
|
|
6
6
|
import pytest
|
|
7
|
-
from box import Box
|
|
7
|
+
from box import Box, BoxList
|
|
8
8
|
from jsonpath import jsonpath
|
|
9
9
|
|
|
10
10
|
from framework.exit_code import ExitCode
|
|
@@ -15,7 +15,12 @@ from framework.global_attribute import CONTEXT
|
|
|
15
15
|
|
|
16
16
|
class Extract(object):
|
|
17
17
|
def __init__(self, response, belong_app):
|
|
18
|
-
|
|
18
|
+
if isinstance(response.json(), dict):
|
|
19
|
+
self.response = Box(response.json())
|
|
20
|
+
elif isinstance(response.json(), list):
|
|
21
|
+
self.response = BoxList(response.json())
|
|
22
|
+
else:
|
|
23
|
+
self.response = response
|
|
19
24
|
self.belong_app = belong_app
|
|
20
25
|
self.context = CONTEXT
|
|
21
26
|
|
|
@@ -48,13 +53,13 @@ class Extract(object):
|
|
|
48
53
|
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
49
54
|
self.belong_app, key = key.split(".", 1)
|
|
50
55
|
|
|
51
|
-
extract_value = jsonpath(self.response
|
|
56
|
+
extract_value = jsonpath(self.response, expression)[0]
|
|
52
57
|
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
|
53
58
|
self.context.set(key, extract_value, self.belong_app)
|
|
54
59
|
logger.info(f"后置提取变量: {key}: {extract_value}")
|
|
55
60
|
|
|
56
61
|
except Exception as e:
|
|
57
|
-
logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response
|
|
62
|
+
logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response}")
|
|
58
63
|
logger.error(f"后置提取变量{key}失败")
|
|
59
64
|
|
|
60
65
|
def extract_by_regex(self, key, reg_expression):
|
|
@@ -76,9 +81,9 @@ class Extract(object):
|
|
|
76
81
|
def extract_by_box(self, key, expression):
|
|
77
82
|
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
78
83
|
self.belong_app, key = key.split(".", 1)
|
|
79
|
-
extract_value = self.get_nested_value(Box(self.response
|
|
84
|
+
extract_value = self.get_nested_value(Box(self.response), expression)
|
|
80
85
|
if not extract_value:
|
|
81
|
-
logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response
|
|
86
|
+
logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response}")
|
|
82
87
|
logger.error(f"后置提取变量{key}失败")
|
|
83
88
|
else:
|
|
84
89
|
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/http_client.py
RENAMED
|
@@ -8,10 +8,13 @@ import json as built_json
|
|
|
8
8
|
from box import Box, BoxList
|
|
9
9
|
from jsonpath import jsonpath as jp
|
|
10
10
|
from requests.models import Response
|
|
11
|
+
from requests_toolbelt import MultipartEncoder
|
|
12
|
+
|
|
11
13
|
from framework.extract import Extract
|
|
14
|
+
from framework.validate import Validate
|
|
12
15
|
from framework.utils.log_util import logger
|
|
13
16
|
from framework.global_attribute import CONTEXT
|
|
14
|
-
from
|
|
17
|
+
from config.settings import CONSOLE_DETAILED_LOG
|
|
15
18
|
from framework.utils.common import convert_numbers_to_decimal
|
|
16
19
|
|
|
17
20
|
|
|
@@ -111,7 +114,8 @@ class HttpClient(object):
|
|
|
111
114
|
with allure.step(f"请求method: {request_obj.get('method')}"):
|
|
112
115
|
logger.info(f"请求method: {request_obj.get('method')}")
|
|
113
116
|
with allure.step(f"请求headers: {request_obj.get('headers')}"):
|
|
114
|
-
|
|
117
|
+
if CONSOLE_DETAILED_LOG:
|
|
118
|
+
logger.info(f"请求headers: {request_obj.get('headers')}")
|
|
115
119
|
if request_obj.get('params'):
|
|
116
120
|
with allure.step(f"请求参数params: {request_obj.get('params')}"):
|
|
117
121
|
logger.info(f"请求参数params: {request_obj.get('params')}")
|
|
@@ -123,18 +127,19 @@ class HttpClient(object):
|
|
|
123
127
|
logger.info(f"请求参数json: {request_obj.get('json')}")
|
|
124
128
|
|
|
125
129
|
with allure.step("响应结果"):
|
|
126
|
-
with allure.step(f"响应code: {self.response.status_code}"):
|
|
127
|
-
logger.info(f"响应code: {self.response.status_code}")
|
|
130
|
+
with allure.step(f"响应status code: {self.response.status_code}"):
|
|
131
|
+
logger.info(f"响应status code: {self.response.status_code}")
|
|
128
132
|
with allure.step(f"响应headers: {self.response.headers}"):
|
|
129
|
-
|
|
133
|
+
if CONSOLE_DETAILED_LOG:
|
|
134
|
+
logger.info(f"响应headers: {self.response.headers}")
|
|
130
135
|
with allure.step(f"响应body: {self.response.json()}"):
|
|
131
136
|
logger.info(f"响应body: {self.response.json()}")
|
|
132
137
|
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
# 断言
|
|
139
|
+
validates = data.get("validate")
|
|
140
|
+
if validates:
|
|
141
|
+
with allure.step("结果断言"):
|
|
142
|
+
Validate(self.response).valid(validates)
|
|
138
143
|
|
|
139
144
|
# 提取变量
|
|
140
145
|
expressions = data.get("extract")
|
|
@@ -72,9 +72,12 @@ def create_test_data(app):
|
|
|
72
72
|
{
|
|
73
73
|
"scenario": {
|
|
74
74
|
"data": {
|
|
75
|
-
"tag_id":
|
|
75
|
+
"tag_id": 1,
|
|
76
76
|
"user_id": 1
|
|
77
|
-
}
|
|
77
|
+
},
|
|
78
|
+
"exclude": [
|
|
79
|
+
"test_untitled"
|
|
80
|
+
]
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
]
|
|
@@ -86,8 +89,8 @@ def create_test_data(app):
|
|
|
86
89
|
"url": "/login",
|
|
87
90
|
"method": "post",
|
|
88
91
|
"json": {
|
|
89
|
-
"
|
|
90
|
-
"
|
|
92
|
+
"username": "Jerry",
|
|
93
|
+
"password": "${user_id}"
|
|
91
94
|
}
|
|
92
95
|
},
|
|
93
96
|
"extract": [
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/common.py
RENAMED
|
@@ -7,9 +7,9 @@ from typing import Any
|
|
|
7
7
|
from decimal import Decimal
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from urllib.parse import unquote, quote, quote_plus, unquote_plus
|
|
10
|
+
|
|
10
11
|
import pyotp
|
|
11
12
|
import cn2an as c2a
|
|
12
|
-
|
|
13
13
|
from config.settings import CONFIG_DIR
|
|
14
14
|
|
|
15
15
|
|
|
@@ -210,18 +210,32 @@ def get_apps():
|
|
|
210
210
|
os.path.isdir(os.path.join(CONFIG_DIR, name)) and not name.startswith(("__", "."))]
|
|
211
211
|
|
|
212
212
|
|
|
213
|
+
# 匹配合法数字字符串(可含千分位)
|
|
214
|
+
decimal_pattern = re.compile(r'^\d{1,3}(,\d{3})*(\.\d+)?$|^\d+(\.\d+)?$')
|
|
215
|
+
|
|
216
|
+
|
|
213
217
|
def convert_numbers_to_decimal(obj: Any) -> Any:
|
|
214
218
|
if isinstance(obj, str):
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
if decimal_pattern.match(obj):
|
|
220
|
+
int_part = obj.split('.')[0]
|
|
221
|
+
if len(int_part) > 10 and "," not in int_part:
|
|
222
|
+
return obj # 整数部分超过10位,不转换
|
|
223
|
+
try:
|
|
224
|
+
stripped = obj.replace(",", "")
|
|
225
|
+
return Decimal(stripped)
|
|
226
|
+
except Exception:
|
|
227
|
+
return obj
|
|
228
|
+
else:
|
|
219
229
|
return obj
|
|
220
230
|
elif isinstance(obj, dict):
|
|
221
231
|
return {k: convert_numbers_to_decimal(v) for k, v in obj.items()}
|
|
222
232
|
elif isinstance(obj, list):
|
|
223
233
|
return [convert_numbers_to_decimal(item) for item in obj]
|
|
224
234
|
elif isinstance(obj, (int, float)) and not isinstance(obj, bool):
|
|
235
|
+
# 处理原生数值类型
|
|
236
|
+
int_part = str(obj).split('.')[0]
|
|
237
|
+
if len(int_part) > 10:
|
|
238
|
+
return obj # 整数部分超过10位,不转换
|
|
225
239
|
return Decimal(str(obj))
|
|
226
240
|
else:
|
|
227
241
|
return obj
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
import allure
|
|
4
|
-
from
|
|
4
|
+
from box import Box, BoxList
|
|
5
5
|
from jsonpath import jsonpath
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
from framework.utils.log_util import logger
|
|
8
|
+
from framework.utils.common import an2cn, is_digit
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class Validate(object):
|
|
10
12
|
def __init__(self, response):
|
|
11
|
-
|
|
13
|
+
if isinstance(response.json(), dict):
|
|
14
|
+
self.response = Box(response.json())
|
|
15
|
+
elif isinstance(response.json(), list):
|
|
16
|
+
self.response = BoxList(response.json())
|
|
17
|
+
else:
|
|
18
|
+
self.response = response
|
|
12
19
|
|
|
13
20
|
self.mapping = {
|
|
14
21
|
"eq": "assert_equal",
|
|
@@ -43,13 +50,13 @@ class Validate(object):
|
|
|
43
50
|
logger.info(
|
|
44
51
|
f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过")
|
|
45
52
|
else:
|
|
46
|
-
|
|
53
|
+
logger.error(f"不支持的断言方式: {key}")
|
|
47
54
|
except AssertionError as e:
|
|
48
55
|
with allure.step(
|
|
49
56
|
f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}"):
|
|
50
57
|
logger.error(
|
|
51
58
|
f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}")
|
|
52
|
-
|
|
59
|
+
logger.error(e)
|
|
53
60
|
|
|
54
61
|
def parse_expectant_expression(self, expectant_expression):
|
|
55
62
|
|
|
@@ -61,17 +68,19 @@ class Validate(object):
|
|
|
61
68
|
elif expectant_expression.startswith("/") and expectant_expression.endswith("/"):
|
|
62
69
|
return self.exec_reg(expectant_expression[1: -1])
|
|
63
70
|
else:
|
|
64
|
-
#
|
|
65
|
-
return self.
|
|
71
|
+
# box句点表达式
|
|
72
|
+
return self.exec_box(expectant_expression)
|
|
73
|
+
|
|
66
74
|
except Exception as e:
|
|
67
|
-
with allure.step(
|
|
68
|
-
logger.error(
|
|
75
|
+
with allure.step(str(e)):
|
|
76
|
+
logger.error(str(e))
|
|
77
|
+
raise Exception(e)
|
|
69
78
|
|
|
70
79
|
def exec_jsonpath(self, expression):
|
|
71
80
|
try:
|
|
72
|
-
return jsonpath(self.response
|
|
73
|
-
except Exception:
|
|
74
|
-
raise Exception(f"jsonpath
|
|
81
|
+
return jsonpath(self.response, expression)[0]
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise Exception(f"jsonpath表达式错误或非预期响应内容{e} 表达式: {expression};响应内容: {self.response}")
|
|
75
84
|
|
|
76
85
|
def exec_reg(self, reg_expression):
|
|
77
86
|
try:
|
|
@@ -80,14 +89,29 @@ class Validate(object):
|
|
|
80
89
|
return eval(result)
|
|
81
90
|
else:
|
|
82
91
|
return result
|
|
83
|
-
except AttributeError:
|
|
84
|
-
raise Exception(f"
|
|
92
|
+
except AttributeError as e:
|
|
93
|
+
raise Exception(f"正则表达式或非预期响应内容{e} 表达式: {reg_expression}; 响应内容: {self.response.text}")
|
|
85
94
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
raise Exception(f"
|
|
95
|
+
def exec_box(self, expression):
|
|
96
|
+
try:
|
|
97
|
+
return self.get_nested_value(Box(self.response), expression)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise Exception(f"box表达式或响应内容异常{e} 表达式: {expression}; 响应内容: {self.response.text}")
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def get_nested_value(obj, attr_path):
|
|
103
|
+
"""通过字符串路径(如 'a.b[0].c')获取嵌套属性值"""
|
|
104
|
+
# 使用正则表达式分解路径,支持属性和索引的组合
|
|
105
|
+
path_elements = re.findall(r'(\w+)|\[(\d+)]', attr_path)
|
|
106
|
+
try:
|
|
107
|
+
for attr, index in path_elements:
|
|
108
|
+
if attr: # 属性部分
|
|
109
|
+
obj = getattr(obj, attr)
|
|
110
|
+
if index: # 索引部分
|
|
111
|
+
obj = obj[int(index)]
|
|
112
|
+
return obj
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
91
115
|
|
|
92
116
|
def assert_equal(self, expectant_expression, practical_result):
|
|
93
117
|
expectant_result = self.parse_expectant_expression(expectant_expression)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-api-framework-alpha
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Author: alpha
|
|
5
5
|
Author-email:
|
|
6
6
|
Requires-Python: >=3.6
|
|
@@ -22,8 +22,6 @@ Requires-Dist: redis==3.5.3
|
|
|
22
22
|
Requires-Dist: requests==2.25.1
|
|
23
23
|
Requires-Dist: requests-toolbelt==1.0.0
|
|
24
24
|
Requires-Dist: retry==0.9.2
|
|
25
|
-
Requires-Dist: pytest-rerunfailures==11.1
|
|
26
|
-
Requires-Dist: pytest-timeout==2.2.0
|
|
27
25
|
Requires-Dist: dill==0.3.8
|
|
28
26
|
Dynamic: author
|
|
29
27
|
Dynamic: requires-dist
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="pytest-api-framework-alpha", # 包名(必须唯一)
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.6",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
author="alpha",
|
|
8
8
|
author_email="",
|
|
@@ -27,8 +27,6 @@ setup(
|
|
|
27
27
|
"requests==2.25.1",
|
|
28
28
|
"requests-toolbelt==1.0.0",
|
|
29
29
|
"retry==0.9.2",
|
|
30
|
-
"pytest-rerunfailures==11.1",
|
|
31
|
-
"pytest-timeout==2.2.0",
|
|
32
30
|
"dill==0.3.8"
|
|
33
31
|
]
|
|
34
32
|
)
|
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/allure_report.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/mysql_db.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/db/redis_db.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/exit_code.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/global_attribute.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/render_data.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/encrypt.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/log_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/teams_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.1.4 → pytest_api_framework_alpha-0.1.6}/framework/utils/yaml_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|