pytest-api-framework-alpha 0.2.2__tar.gz → 0.2.3__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 (30) hide show
  1. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/PKG-INFO +1 -1
  2. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/allure_report.py +1 -1
  3. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/base_class.py +5 -6
  4. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/conftest.py +25 -4
  5. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/db/redis_db.py +40 -23
  6. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/global_attribute.py +20 -3
  7. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/http_client.py +16 -14
  8. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/render_data.py +21 -21
  9. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/report.py +1 -1
  10. pytest_api_framework_alpha-0.2.3/framework/settings.py +30 -0
  11. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/startapp.py +2 -1
  12. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/common.py +23 -14
  13. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/validate.py +84 -19
  14. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/pytest_api_framework_alpha.egg-info/PKG-INFO +1 -1
  15. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/pytest_api_framework_alpha.egg-info/SOURCES.txt +1 -0
  16. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/setup.py +1 -1
  17. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/__init__.py +0 -0
  18. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/db/__init__.py +0 -0
  19. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/db/mysql_db.py +0 -0
  20. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/exit_code.py +0 -0
  21. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/extract.py +0 -0
  22. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/__init__.py +0 -0
  23. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/encrypt.py +0 -0
  24. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/log_util.py +0 -0
  25. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/teams_util.py +0 -0
  26. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/framework/utils/yaml_util.py +0 -0
  27. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
  28. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/pytest_api_framework_alpha.egg-info/requires.txt +0 -0
  29. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
  30. {pytest_api_framework_alpha-0.2.2 → pytest_api_framework_alpha-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -3,7 +3,7 @@ import os
3
3
  import platform
4
4
  from framework.utils.log_util import logger
5
5
  from framework.global_attribute import CONTEXT
6
- from config.settings import ALLURE_DIR, ALLURE_REPORT_DIR, ALLURE_RESULTS_DIR, ALLURE_ENV_PROPERTIES
6
+ from framework.settings import ALLURE_DIR, ALLURE_REPORT_DIR, ALLURE_RESULTS_DIR, ALLURE_ENV_PROPERTIES
7
7
 
8
8
 
9
9
  def generate_report():
@@ -5,16 +5,16 @@ from urllib.parse import urlparse, urlunparse, urljoin
5
5
 
6
6
  import pytest
7
7
  from box import Box
8
- from faker import Faker
9
8
 
10
9
  from framework.exit_code import ExitCode
11
10
  from framework.db.redis_db import RedisDB
11
+ from framework.db.mysql_db import MysqlDB
12
12
  from framework.utils.log_util import logger
13
13
  from framework.render_data import RenderData
14
14
  from framework.http_client import ResponseUtil
15
+ from framework.settings import UNAUTHORIZED_CODE, FAKER_LANGUAGE
15
16
  from framework.utils.common import snake_to_pascal, SingletonFaker
16
17
  from framework.global_attribute import CONFIG, GlobalAttribute, _FRAMEWORK_CONTEXT
17
- from config.settings import UNAUTHORIZED_CODE, FAKER_LANGUAGE
18
18
 
19
19
  module = importlib.import_module("test_case.conftest")
20
20
 
@@ -36,7 +36,6 @@ class BaseTestCase(object):
36
36
  app_http = getattr(self.http, app)
37
37
  domain = self.context.get(app).get("domain")
38
38
  data = RenderData(data).render()
39
- url = data.request.get("url")
40
39
  data.request.url = self.replace_domain(data.request.url, domain)
41
40
  self.response = getattr(app_http, account).request(data=data, kwargs=kwargs)
42
41
  if self.response.status_code in UNAUTHORIZED_CODE:
@@ -76,16 +75,16 @@ class BaseTestCase(object):
76
75
  request.update({"method": "delete", "headers": {}, **kwargs})
77
76
  return self.request(app=app, account=account, data=Box({"request": request}))
78
77
 
79
- def mysql_conn(self, db, app=None):
78
+ def mysql_conn(self, db, app=None) -> MysqlDB:
80
79
  try:
81
80
  return _FRAMEWORK_CONTEXT.get(app=self.default_app(app), key="mysql").get(db)
82
81
  except AttributeError as e:
83
82
  traceback.print_exc()
84
83
  pytest.exit(ExitCode.LOAD_DATABASE_INFO_ERROR)
85
84
 
86
- def redis_conn(self, db, app=None):
85
+ def redis_conn(self, db, index=0, app=None) -> RedisDB:
87
86
  try:
88
- return RedisDB(**CONFIG.get(app=self.default_app(app), key="redis").get(db))
87
+ return _FRAMEWORK_CONTEXT.get(app=self.default_app(app), key="redis").get(db)[index]
89
88
  except AttributeError as e:
90
89
  traceback.print_exc()
91
90
  pytest.exit(ExitCode.LOAD_DATABASE_INFO_ERROR)
@@ -22,12 +22,12 @@ from framework.exit_code import ExitCode
22
22
  from framework.db.mysql_db import MysqlDB
23
23
  from framework.db.redis_db import RedisDB
24
24
  from framework.utils.log_util import logger
25
- from framework.render_data import RenderData
25
+ # from framework.render_data import RenderData
26
26
  from framework.utils.yaml_util import YamlUtil
27
+ from framework.settings import DATA_DIR, CASES_DIR
27
28
  from framework.allure_report import generate_report
28
29
  from framework.global_attribute import CONTEXT, CONFIG, _FRAMEWORK_CONTEXT
29
30
  from framework.utils.common import snake_to_pascal, get_apps, convert_numbers_to_decimal
30
- from config.settings import DATA_DIR, CASES_DIR
31
31
 
32
32
  all_app = get_apps()
33
33
  module = importlib.import_module("test_case.conftest")
@@ -139,6 +139,12 @@ def pytest_generate_tests(metafunc):
139
139
  if not case_common.get("url"):
140
140
  logger.warning(f"{func_file_path} request中缺少必填字段: url", case_data)
141
141
  # pytest.exit(ExitCode.YAML_MISSING_FIELDS)
142
+ else:
143
+ url = case_common.get("url")
144
+ if url.strip().startswith("${"):
145
+ case_data["request"]["url"] = url
146
+ else:
147
+ case_data["request"]["url"] = urljoin(domain, url)
142
148
  else:
143
149
  if url.strip().startswith("${"):
144
150
  case_data["request"]["url"] = url
@@ -150,6 +156,8 @@ def pytest_generate_tests(metafunc):
150
156
  if not case_common.get("method"):
151
157
  logger.warning(f"{func_file_path} request中缺少必填字段: method", case_data)
152
158
  # pytest.exit(ExitCode.YAML_MISSING_FIELDS)
159
+ else:
160
+ case_data["request"]["method"] = case_common.get("method")
153
161
 
154
162
  for key in ["title", "level"]:
155
163
  if key not in case_data:
@@ -179,8 +187,9 @@ def pytest_generate_tests(metafunc):
179
187
  # 剔除标记disable的字段
180
188
  deep_copied_case_data = disable_field(scenario.get("data"), deep_copied_case_data)
181
189
  deep_copied_case_data["_scenario"] = item.get("scenario")
190
+ deep_copied_case_data["_ignore_failed"] = case_common.get("ignore_failed", False)
182
191
  case_data_list.append(deep_copied_case_data)
183
- ids.append(case_data.get("title") + f"#{index + 1}")
192
+ ids.append(f'{case_data.get("title")} - {scenario.get("describe", "")}#{index + 1}')
184
193
  except KeyError as e:
185
194
  logger.error(f"scenario参数化格式不正确:{e}")
186
195
  traceback.print_exc()
@@ -189,6 +198,7 @@ def pytest_generate_tests(metafunc):
189
198
  else:
190
199
  if not case_common.get("ignore"):
191
200
  case_data["_scenario"] = {"data": {}}
201
+ case_data["_ignore_failed"] = case_common.get("ignore_failed", False)
192
202
  case_data_list = [case_data]
193
203
  # 进行参数化生成用例
194
204
  metafunc.parametrize("data", case_data_list, ids=[f'{case_data.get("title")}#1'], scope="function")
@@ -408,9 +418,11 @@ def pytest_runtest_call(item):
408
418
  :param item:
409
419
  :return:
410
420
  """
421
+ origin_data = item.funcargs.get("data")
411
422
  func_name = item.originalname
423
+ ignore_failed = origin_data.get("_ignore_failed")
412
424
  ignore_error_and_continue = item.config.getini("ignore_error_and_continue")
413
- if ignore_error_and_continue == "false":
425
+ if not (ignore_failed is True or ignore_error_and_continue == "true"):
414
426
  # setup方法执行失败,则主动标记用例执行失败,不会执行用例
415
427
  if item.funcargs.get("setup_success") is False:
416
428
  pytest.skip("test_before_scenario execute error")
@@ -590,6 +602,15 @@ def login():
590
602
  mysqls = CONFIG.get(app=app, key="mysql")
591
603
  mysqls_obj = {item: MysqlDB(**mysqls.get(item)) for item in mysqls.keys()}
592
604
  _FRAMEWORK_CONTEXT.set(app=app, key="mysql", value=mysqls_obj)
605
+ redis = CONFIG.get(app=app, key="redis")
606
+ redis_obj = {
607
+ db: [
608
+ RedisDB(**{**db_info, "db": i})
609
+ for i in range(16)
610
+ ]
611
+ for db, db_info in redis.items()
612
+ }
613
+ _FRAMEWORK_CONTEXT.set(app=app, key="redis", value=redis_obj)
593
614
 
594
615
  try:
595
616
  logger.info("登录账号".center(80, "*"))
@@ -7,7 +7,7 @@ from framework.utils.log_util import logger
7
7
 
8
8
  class RedisDB(Redis):
9
9
 
10
- def __init__(self, host, port, password, db, max_connections=10):
10
+ def __init__(self, host, port, password, db, max_connections=1):
11
11
  super(RedisDB, self).__init__()
12
12
  self.db = db
13
13
  self.max_connections = max_connections
@@ -56,7 +56,7 @@ class RedisDB(Redis):
56
56
 
57
57
  if value:
58
58
  if isinstance(value, dict):
59
- self.push(name, value)
59
+ self.conn.lpush(name, json.dumps(value, ensure_ascii=False))
60
60
  elif isinstance(value, list):
61
61
  if isinstance(value[0], dict):
62
62
  value = [json.dumps(item, ensure_ascii=False) for item in value]
@@ -102,41 +102,58 @@ class RedisDB(Redis):
102
102
  def set_ttl(self, name, time):
103
103
  res = self.conn.expire(name, time)
104
104
  logger.info(f'expire {name} {time} ---> {res}')
105
- if res:
106
- return res
107
- else:
108
- return False
105
+ return res
109
106
 
110
107
  def exists(self, name):
111
108
  res = self.conn.exists(name)
112
109
  logger.info(f'exists {name} ---> {res}')
113
- if res:
114
- return True
115
- else:
116
- return False
110
+ return res
117
111
 
118
112
  def delete(self, name):
119
113
  res = self.conn.delete(name)
120
114
  logger.info(f'delete {name} ---> {res}')
121
- if res:
122
- return True
123
- else:
124
- return False
115
+ return res
125
116
 
126
- def push(self, name, value):
117
+ def lpush(self, name, *value):
127
118
  """
128
- 通过list实现队列,从左侧推消息,消息体只能字典
119
+ 通过list实现队列,从左侧推消息
129
120
  :param name:
130
121
  :param value:
131
122
  :return:
132
123
  """
133
- res = self.conn.lpush(name, json.dumps(value, ensure_ascii=False))
124
+ res = self.conn.lpush(name, *value)
134
125
  logger.info(f'lpush {name} ---> {value}')
135
- if res:
136
- return True
137
- else:
138
- return False
126
+ return res
127
+
128
+ def rpush(self, name, *value):
129
+ """
130
+ 通过list实现队列,从右侧推消息
131
+ :param name:
132
+ :param value:
133
+ :return:
134
+ """
135
+ res = self.conn.rpush(name, *value)
136
+ logger.info(f'rpush {name} ---> {value}')
137
+ return res
139
138
 
139
+ def lpop(self, queue_name):
140
+ logger.info(f'lpop {queue_name}')
141
+ return self.conn.lpop(queue_name)
140
142
 
141
- if __name__ == '__main__':
142
- redis = RedisDB(db=0)
143
+ def rpop(self, queue_name):
144
+ logger.info(f'rpop {queue_name}')
145
+ return self.conn.rpop(queue_name)
146
+
147
+ def lpop_all(self, queue_name):
148
+ """弹出并返回队列中所有元素(会清空队列)"""
149
+ all_elements = []
150
+ try:
151
+ while True:
152
+ element = self.conn.lpop(queue_name)
153
+ if element is None:
154
+ break # 队列为空时退出循环
155
+ all_elements.append(element)
156
+ return all_elements
157
+ except Exception as e:
158
+ print(f"获取所有元素失败: {e}")
159
+ return []
@@ -1,21 +1,38 @@
1
1
  import os
2
2
  import traceback
3
- import yaml
3
+ from pathlib import Path
4
4
 
5
+ import yaml
5
6
  import pytest
6
7
  from box import Box
8
+ from dotenv import set_key
7
9
  from box.exceptions import BoxError
8
10
 
9
- from config.settings import ROOT_DIR
10
11
  from framework.exit_code import ExitCode
11
12
  from framework.utils.log_util import logger
12
- from framework.utils.common import singleton
13
+ from framework.settings import ROOT_DIR
13
14
 
14
15
 
15
16
  class NoDatesSafeLoader(yaml.SafeLoader):
16
17
  pass
17
18
 
18
19
 
20
+ def singleton(cls):
21
+ """
22
+ 单例模式装饰器
23
+ :param cls:
24
+ :return:
25
+ """
26
+ instances = {}
27
+
28
+ def get_instance(*args, **kwargs):
29
+ if cls not in instances:
30
+ instances[cls] = cls(*args, **kwargs)
31
+ return instances[cls]
32
+
33
+ return get_instance
34
+
35
+
19
36
  # 禁用 YAML 中的 timestamp 类型自动转换
20
37
  for ch in list(NoDatesSafeLoader.yaml_implicit_resolvers):
21
38
  resolvers = NoDatesSafeLoader.yaml_implicit_resolvers[ch]
@@ -14,7 +14,7 @@ from framework.extract import Extract
14
14
  from framework.validate import Validate
15
15
  from framework.utils.log_util import logger
16
16
  from framework.global_attribute import CONTEXT
17
- from config.settings import CONSOLE_DETAILED_LOG
17
+ from framework.settings import CONSOLE_DETAILED_LOG
18
18
  from framework.utils.common import convert_numbers_to_decimal
19
19
 
20
20
 
@@ -113,33 +113,35 @@ class HttpClient(object):
113
113
  logger.info(f"请求url: {request_obj.get('url')}")
114
114
  with allure.step(f"请求method: {request_obj.get('method')}"):
115
115
  logger.info(f"请求method: {request_obj.get('method')}")
116
- with allure.step(f"请求headers: {request_obj.get('headers')}"):
116
+ with allure.step(f"请求headers: {built_json.dumps(request_obj.get('headers'))}"):
117
117
  if CONSOLE_DETAILED_LOG:
118
- logger.info(f"请求headers: {request_obj.get('headers')}")
118
+
119
+ logger.info(f"请求headers: {built_json.dumps(request_obj.get('headers'))}")
119
120
  if request_obj.get('params'):
120
- with allure.step(f"请求参数params: {request_obj.get('params')}"):
121
- logger.info(f"请求参数params: {request_obj.get('params')}")
121
+ with allure.step(f"请求参数params: {built_json.dumps(request_obj.get('params'))}"):
122
+ logger.info(f"请求参数params: {built_json.dumps(request_obj.get('params'))}")
122
123
  if request_obj.get('data'):
123
- with allure.step(f"请求参数data: {request_obj.get('data')}"):
124
- logger.info(f"请求参数data: {request_obj.get('data')}")
124
+ with allure.step(f"请求参数data: {built_json.dumps(request_obj.get('data'))}"):
125
+ logger.info(f"请求参数data: {built_json.dumps(request_obj.get('data'))}")
125
126
  if request_obj.get('json'):
126
- with allure.step(f"请求参数json: {request_obj.get('json')}"):
127
- logger.info(f"请求参数json: {request_obj.get('json')}")
127
+ with allure.step(f"请求参数json: {built_json.dumps(request_obj.get('json'))}"):
128
+ logger.info(f"请求参数json: {built_json.dumps(request_obj.get('json'))}")
128
129
 
129
130
  with allure.step("响应结果"):
130
131
  with allure.step(f"响应status code: {self.response.status_code}"):
131
132
  logger.info(f"响应status code: {self.response.status_code}")
132
- with allure.step(f"响应headers: {self.response.headers}"):
133
+
134
+ with allure.step(f"响应headers: {built_json.dumps(dict(self.response.headers))}"):
133
135
  if CONSOLE_DETAILED_LOG:
134
- logger.info(f"响应headers: {self.response.headers}")
135
- with allure.step(f"响应body: {self.response.json()}"):
136
- logger.info(f"响应body: {self.response.json()}")
136
+ logger.info(f"响应headers: {built_json.dumps(dict(self.response.headers))}")
137
+ with allure.step(f"响应body: {built_json.dumps(self.response.json())}"):
138
+ logger.info(f"响应body: {built_json.dumps(self.response.json())}")
137
139
 
138
140
  # 断言
139
141
  validates = data.get("validate")
140
142
  if validates:
141
143
  with allure.step("结果断言"):
142
- Validate(self.response).valid(validates)
144
+ Validate(data, self.response).valid(validates)
143
145
 
144
146
  # 提取变量
145
147
  expressions = data.get("extract")
@@ -9,8 +9,8 @@ from box import Box
9
9
  from framework.exit_code import ExitCode
10
10
  from framework.utils.log_util import logger
11
11
  from framework.global_attribute import CONTEXT
12
- from framework.utils.common import SingletonFaker
13
- from config.settings import FAKER_LANGUAGE, DATA_DIR
12
+ from framework.settings import FAKER_LANGUAGE, DATA_DIR
13
+ from framework.utils.common import SingletonFaker, remove_spaces
14
14
 
15
15
  module = importlib.import_module("framework.utils.common")
16
16
 
@@ -34,11 +34,23 @@ class RenderData(object):
34
34
  """
35
35
  with allure.step("渲染数据"):
36
36
  self.replace_attribute(self.scenario)
37
+ self.render_url()
37
38
  self.replace_attribute(self.request)
38
39
  self.data["request"] = self.request
39
40
  self.data["_is_multipart"] = self._is_multipart
40
41
  return self.data
41
42
 
43
+ def render_url(self):
44
+ url = remove_spaces(self.request.url)
45
+ pattern = re.compile(r"\$\{([\w.\[\]0-9]+(?:\(\w*(?:,\w*)*\))?)}")
46
+
47
+ def replacer(match):
48
+ item = match.group(1) # 拿到占位符里的内容
49
+ value = self.get_attribute(item)
50
+ return str(value) if value is not None else match.group(0) # 未取到就保留原样
51
+
52
+ self.request.url = pattern.sub(replacer, url)
53
+
42
54
  def replace_attribute(self, data):
43
55
  pattern = re.compile(r"\$\{([\w.\[\]0-9]+(?:\(\w*(?:,\w*)*\))?)}")
44
56
  file_path_pattern = re.compile(
@@ -52,6 +64,7 @@ class RenderData(object):
52
64
  self.replace_attribute(value)
53
65
  # 如果是字符串类型并匹配正则表达式,则替换
54
66
  elif isinstance(value, str):
67
+ value = remove_spaces(value)
55
68
  if pattern.search(value):
56
69
  data[key] = self.get_attribute(value[2:-1])
57
70
  elif file_path_pattern.search(value):
@@ -66,6 +79,7 @@ class RenderData(object):
66
79
  self.replace_attribute(item)
67
80
  # 如果是字符串类型并匹配正则表达式,则替换
68
81
  elif isinstance(item, str):
82
+ item = remove_spaces(item)
69
83
  if pattern.search(item):
70
84
  data[index] = self.get_attribute(item[2:-1])
71
85
  elif file_path_pattern.search(item):
@@ -73,7 +87,7 @@ class RenderData(object):
73
87
  self._is_multipart = True
74
88
 
75
89
  def get_attribute(self, keyword):
76
- if not keyword.endswith(")"):
90
+ if not keyword.strip().endswith(")"):
77
91
  return self.get_attribute_variable(keyword)
78
92
  # 如果关键字是函数
79
93
  else:
@@ -96,7 +110,7 @@ class RenderData(object):
96
110
  :param expression:
97
111
  :return:
98
112
  """
99
- if not expression.startswith(tuple(f"{app}." for app in self.context.all_app)):
113
+ if not expression.strip().startswith(tuple(f"{app}." for app in self.context.all_app)):
100
114
  belong_app = self.data.get('_belong_app')
101
115
  new_expression = f"{belong_app}.{expression}"
102
116
  else:
@@ -108,23 +122,9 @@ class RenderData(object):
108
122
  if self.get_nested_value(self.context, new_expression) is not None else
109
123
  self.get_nested_value(self.context, expression)
110
124
  )
111
- # if value.endswith(")}"):
112
- # keyword = value[2:-1]
113
- # try:
114
- # pattern = re.compile(r'(?P<func_name>.+)\((?P<args>.*)\)')
115
- # match = re.match(pattern, keyword)
116
- # # 匹配方法名
117
- # func_name = match.group("func_name")
118
- # # 匹配方法位置参数
119
- # args = match.group("args")
120
- # if args:
121
- # # 将参数中字符串类型的数字转成数字类型
122
- # args = [eval(i) for i in args.replace(",", "").split(",")]
123
- # value = self.get_func_variable(keyword, func_name, *args)
124
- # else:
125
- # value = self.get_func_variable(keyword, func_name)
126
- # except Exception:
127
- # pass
125
+ if isinstance(value, str) and value.strip().startswith("${") and value.strip().endswith("}"):
126
+ value = self.get_nested_value(self.context, new_expression) or self.get_nested_value(self.context,
127
+ expression)
128
128
 
129
129
  with allure.step(f"{expression}: {value}"):
130
130
  logger.info(f"前置读取变量: {expression}: {value}")
@@ -4,7 +4,7 @@ import datetime
4
4
  import traceback
5
5
  from pathlib import Path
6
6
  from framework.db.mysql_db import MysqlDB
7
- from config.settings import DATABASE_HOST, DATABASE_PASSWORD, DATABASE_DB, DATABASE_USERNAME, DATABASE_PORT
7
+ from framework.settings import DATABASE_HOST, DATABASE_PASSWORD, DATABASE_DB, DATABASE_USERNAME, DATABASE_PORT
8
8
 
9
9
 
10
10
  def timestamp_to_datetime_str(timestamp):
@@ -0,0 +1,30 @@
1
+ import os
2
+
3
+ ROOT_DIR = os.getcwd()
4
+ # 生成allure报告程序的路径
5
+ ALLURE_DIR = os.getenv("ALLURE_DIR")
6
+ # allure统计执行结果目录
7
+ ALLURE_RESULTS_DIR = os.path.join(ROOT_DIR, "allure_results")
8
+ # allure报告目录
9
+ ALLURE_REPORT_DIR = os.path.join(ROOT_DIR, "allure_report")
10
+ # allure报告统计执行环境的存放路径
11
+ ALLURE_ENV_PROPERTIES = os.path.join(ALLURE_RESULTS_DIR, "environment.properties")
12
+ # 存放case的目录
13
+ CASES_DIR = os.path.join(ROOT_DIR, "test_case")
14
+ # 存放测试数据的目录
15
+ DATA_DIR = os.path.join(ROOT_DIR, "test_data")
16
+ # 存放配置的目录
17
+ CONFIG_DIR = os.path.join(ROOT_DIR, "config")
18
+
19
+ FAKER_LANGUAGE = "en_US"
20
+ # 是否记录详细日志
21
+ CONSOLE_DETAILED_LOG = False
22
+ # 鉴权失败返回的http code
23
+ UNAUTHORIZED_CODE = [401]
24
+ # response属性
25
+ RESPONSE_ATTR = ["status_code", "url", "ok", "encoding"]
26
+
27
+ try:
28
+ from config.settings import *
29
+ except ImportError:
30
+ pass
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
 
5
5
  import yaml
6
6
  from framework.utils.common import snake_to_pascal
7
- from config.settings import CONFIG_DIR, CASES_DIR, DATA_DIR
7
+ from framework.settings import CONFIG_DIR, CASES_DIR, DATA_DIR
8
8
 
9
9
 
10
10
  def create_yaml(app):
@@ -68,6 +68,7 @@ def create_test_data(app):
68
68
  "case_common": {
69
69
  "module": "功能模块名称",
70
70
  "describe": "测试场景描述",
71
+ "ignore_failed": False, # 默认false,遇到失败case会忽略当前类中后续case,直接执行下个测试类
71
72
  "scenarios": [
72
73
  {
73
74
  "scenario": {
@@ -11,8 +11,27 @@ from urllib.parse import unquote, quote, quote_plus, unquote_plus
11
11
  import pyotp
12
12
  import cn2an as c2a
13
13
  from faker import Faker
14
+ from framework.settings import CONFIG_DIR
14
15
 
15
- from config.settings import CONFIG_DIR
16
+ try:
17
+ from utils.common import *
18
+ except ImportError:
19
+ pass
20
+
21
+
22
+ class SingletonFaker(object):
23
+ instance = None
24
+ init_flag = False
25
+
26
+ def __init__(self, locale):
27
+ if self.init_flag:
28
+ return
29
+ self.faker = Faker(locale)
30
+
31
+ def __new__(cls, *args, **kwargs):
32
+ if cls.instance is None:
33
+ cls.instance = super().__new__(cls)
34
+ return cls.instance
16
35
 
17
36
 
18
37
  def generate_2fa_code(secret_key):
@@ -243,19 +262,9 @@ def convert_numbers_to_decimal(obj: Any) -> Any:
243
262
  return obj
244
263
 
245
264
 
246
- class SingletonFaker(object):
247
- instance = None
248
- init_flag = False
249
-
250
- def __init__(self, locale):
251
- if self.init_flag:
252
- return
253
- self.faker = Faker(locale)
254
-
255
- def __new__(cls, *args, **kwargs):
256
- if cls.instance is None:
257
- cls.instance = super().__new__(cls)
258
- return cls.instance
265
+ def remove_spaces(s: str) -> str:
266
+ """去掉字符串中的所有空格"""
267
+ return s.replace(" ", "")
259
268
 
260
269
 
261
270
  if __name__ == '__main__':
@@ -1,21 +1,20 @@
1
1
  import re
2
2
 
3
3
  import allure
4
+ import pytest
4
5
  from box import Box, BoxList
5
6
  from jsonpath import jsonpath
6
7
 
7
8
  from framework.utils.log_util import logger
9
+ from framework.render_data import RenderData
10
+ from framework.settings import RESPONSE_ATTR
8
11
  from framework.utils.common import an2cn, is_digit
9
12
 
10
13
 
11
14
  class Validate(object):
12
- def __init__(self, response):
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
15
+ def __init__(self, data, response):
16
+ self.data = data
17
+ self.response = response
19
18
 
20
19
  self.mapping = {
21
20
  "eq": "assert_equal",
@@ -32,7 +31,9 @@ class Validate(object):
32
31
  "contains": "assert_contains",
33
32
  "contained_by": "assert_contained_by",
34
33
  "startswith": "assert_startswith",
35
- "endswith": "assert_endswith"
34
+ "endswith": "assert_endswith",
35
+ "is_null": "assert_is_null",
36
+ "not_null": "assert_is_not_null",
36
37
  }
37
38
 
38
39
  def valid(self, validates):
@@ -40,23 +41,26 @@ class Validate(object):
40
41
  key = list(valid_item.keys())[0]
41
42
  valid_list = [i.strip() for i in valid_item.get(key).split(",")]
42
43
  expression = valid_list[0]
43
- expectant_result = valid_list[1]
44
+ try:
45
+ expectant_result = valid_list[1]
46
+ except IndexError:
47
+ expectant_result = ""
44
48
  func = self.mapping.get(key)
45
49
  try:
46
50
  if func:
47
- getattr(self, func)(expression, expectant_result)
51
+ result = getattr(self, func)(expression, expectant_result.strip())
48
52
  with allure.step(
49
- f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过"):
53
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: {result}"):
50
54
  logger.info(
51
- f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过")
55
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: {result}")
52
56
  else:
53
57
  logger.error(f"不支持的断言方式: {key}")
54
58
  except AssertionError as e:
55
59
  with allure.step(
56
- f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}"):
60
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: {result}。 失败原因: {e}"):
57
61
  logger.error(
58
- f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}")
59
- logger.error(e)
62
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: {result}。 失败原因: {e}")
63
+ pytest.fail(e)
60
64
 
61
65
  def parse_expectant_expression(self, expectant_expression):
62
66
 
@@ -78,7 +82,7 @@ class Validate(object):
78
82
 
79
83
  def exec_jsonpath(self, expression):
80
84
  try:
81
- return jsonpath(self.response, expression)[0]
85
+ return jsonpath(self.response.json(), expression)[0]
82
86
  except Exception as e:
83
87
  raise Exception(f"jsonpath表达式错误或非预期响应内容{e} 表达式: {expression};响应内容: {self.response}")
84
88
 
@@ -94,7 +98,9 @@ class Validate(object):
94
98
 
95
99
  def exec_box(self, expression):
96
100
  try:
97
- return self.get_nested_value(Box(self.response), expression)
101
+ if expression in RESPONSE_ATTR:
102
+ return getattr(self.response, expression)
103
+ return self.get_nested_value(Box(self.response.json()), expression)
98
104
  except Exception as e:
99
105
  raise Exception(f"box表达式或响应内容异常{e} 表达式: {expression}; 响应内容: {self.response.text}")
100
106
 
@@ -116,105 +122,164 @@ class Validate(object):
116
122
  def assert_equal(self, expectant_expression, practical_result):
117
123
  expectant_result = self.parse_expectant_expression(expectant_expression)
118
124
  if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
119
- practical_result = eval(practical_result)
125
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith(
126
+ "}"):
127
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
128
+ else:
129
+ practical_result = eval(practical_result)
120
130
  assert practical_result == expectant_result, f'{expectant_result} == {practical_result}'
131
+ return f'{expectant_result} == {practical_result}'
121
132
 
122
133
  def assert_not_equal(self, expectant_expression, practical_result):
123
134
  expectant_result = self.parse_expectant_expression(expectant_expression)
124
135
  if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
125
- practical_result = eval(practical_result)
136
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith(
137
+ "}"):
138
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
139
+ else:
140
+ practical_result = eval(practical_result)
126
141
  assert practical_result != expectant_result, f'{expectant_result} != {practical_result}'
142
+ return f'{expectant_result} != {practical_result}'
127
143
 
128
144
  def assert_less_than(self, expectant_expression, practical_result):
129
145
  expectant_result, practical_result = self.__to_digit(
130
146
  self.parse_expectant_expression(expectant_expression),
131
147
  practical_result
132
148
  )
149
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
150
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
133
151
  assert expectant_result < practical_result, f'{expectant_result} < {practical_result}'
152
+ return f'{expectant_result} < {practical_result}'
134
153
 
135
154
  def assert_less_than_or_equals(self, expectant_expression, practical_result):
136
155
  expectant_result, practical_result = self.__to_digit(
137
156
  self.parse_expectant_expression(expectant_expression),
138
157
  practical_result
139
158
  )
159
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
160
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
140
161
  assert expectant_result <= practical_result, f'{expectant_result} <= {practical_result}'
162
+ return f'{expectant_result} <= {practical_result}'
141
163
 
142
164
  def assert_greater_than(self, expectant_expression, practical_result):
143
165
  expectant_result, practical_result = self.__to_digit(
144
166
  self.parse_expectant_expression(expectant_expression),
145
167
  practical_result
146
168
  )
169
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
170
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
147
171
  assert expectant_result > practical_result, f'{expectant_result} > {practical_result}'
172
+ return f'{expectant_result} > {practical_result}'
148
173
 
149
174
  def assert_greater_than_or_equals(self, expectant_expression, practical_result):
150
175
  expectant_result, practical_result = self.__to_digit(
151
176
  self.parse_expectant_expression(expectant_expression),
152
177
  practical_result
153
178
  )
179
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
180
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
154
181
  assert expectant_result >= practical_result, f'{expectant_result} >= {practical_result}'
182
+ return f'{expectant_result} >= {practical_result}'
155
183
 
156
184
  def assert_contains(self, expectant_expression, practical_result):
157
185
  expectant_result, practical_result = self.__to_str(
158
186
  self.parse_expectant_expression(expectant_expression),
159
187
  practical_result
160
188
  )
189
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
190
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
161
191
  assert practical_result in expectant_result, f'{practical_result} in {expectant_result}'
192
+ return f'{practical_result} in {expectant_result}'
162
193
 
163
194
  def assert_contained_by(self, expectant_expression, practical_result):
164
195
  expectant_result, practical_result = self.__to_str(
165
196
  self.parse_expectant_expression(expectant_expression),
166
197
  practical_result
167
198
  )
199
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
200
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
168
201
  assert expectant_result in practical_result, f'{expectant_result} in {practical_result}'
202
+ return f'{expectant_result} in {practical_result}'
169
203
 
170
204
  def assert_startswith(self, expectant_expression, practical_result):
171
205
  expectant_result, practical_result = self.__to_str(
172
206
  self.parse_expectant_expression(expectant_expression),
173
207
  practical_result
174
208
  )
209
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
210
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
175
211
  assert expectant_result.startswith(practical_result), f'{expectant_result} 以 {practical_result} 开头'
212
+ return f'{expectant_result} 以 {practical_result} 开头'
176
213
 
177
214
  def assert_endswith(self, expectant_expression, practical_result):
178
215
  expectant_result, practical_result = self.__to_str(
179
216
  self.parse_expectant_expression(expectant_expression),
180
217
  practical_result
181
218
  )
219
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
220
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
182
221
  assert expectant_result.endswith(practical_result), f'{expectant_result} 以 {practical_result} 结尾'
222
+ return f'{expectant_result} 以 {practical_result} 结尾'
183
223
 
184
224
  def assert_length_equals(self, expectant_expression, practical_result):
185
225
  expectant_result, practical_result = self.__to_str(
186
226
  self.parse_expectant_expression(expectant_expression),
187
227
  practical_result
188
228
  )
229
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
230
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
189
231
  assert len(expectant_result) == int(practical_result), f'{len(expectant_result)} == {practical_result}'
232
+ return f'{len(expectant_result)} == {practical_result}'
190
233
 
191
234
  def assert_length_less_than(self, expectant_expression, practical_result):
192
235
  expectant_result, practical_result = self.__to_str(
193
236
  self.parse_expectant_expression(expectant_expression),
194
237
  practical_result
195
238
  )
239
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
240
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
196
241
  assert len(expectant_result) < int(practical_result), f'{len(expectant_result)} < {practical_result}'
242
+ return f'{len(expectant_result)} < {practical_result}'
197
243
 
198
244
  def assert_length_less_than_or_equals(self, expectant_expression, practical_result):
199
245
  expectant_result, practical_result = self.__to_str(
200
246
  self.parse_expectant_expression(expectant_expression),
201
247
  practical_result
202
248
  )
249
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
250
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
203
251
  assert len(expectant_result) <= int(practical_result), f'{len(expectant_result)} <= {practical_result}'
252
+ return f'{len(expectant_result)} <= {practical_result}'
204
253
 
205
254
  def assert_length_greater_than(self, expectant_expression, practical_result):
206
255
  expectant_result, practical_result = self.__to_str(
207
256
  self.parse_expectant_expression(expectant_expression),
208
257
  practical_result
209
258
  )
259
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
260
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
210
261
  assert len(expectant_result) > int(practical_result), f'{len(expectant_result)} > {practical_result}'
262
+ return f'{len(expectant_result)} > {practical_result}'
211
263
 
212
264
  def assert_length_greater_than_or_equals(self, expectant_expression, practical_result):
213
265
  expectant_result, practical_result = self.__to_str(
214
266
  self.parse_expectant_expression(expectant_expression),
215
267
  practical_result
216
268
  )
269
+ if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith("}"):
270
+ practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
217
271
  assert len(expectant_result) >= int(practical_result), f'{len(expectant_result)} >= {practical_result}'
272
+ return f'{len(expectant_result)} >= {practical_result}'
273
+
274
+ def assert_is_null(self, expectant_expression, practical_result):
275
+ expectant_result = self.parse_expectant_expression(expectant_expression)
276
+ assert expectant_result is None
277
+ return expectant_result
278
+
279
+ def assert_is_not_null(self, expectant_expression, practical_result):
280
+ expectant_result = self.parse_expectant_expression(expectant_expression)
281
+ assert expectant_result is not None
282
+ return expectant_result
218
283
 
219
284
  def __to_str(self, expectant_expression, practical_result):
220
285
  if isinstance(expectant_expression, (int, float)):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -9,6 +9,7 @@ framework/global_attribute.py
9
9
  framework/http_client.py
10
10
  framework/render_data.py
11
11
  framework/report.py
12
+ framework/settings.py
12
13
  framework/startapp.py
13
14
  framework/validate.py
14
15
  framework/db/__init__.py
@@ -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.2.2",
5
+ version="0.2.3",
6
6
  packages=find_packages(),
7
7
  author="alpha",
8
8
  author_email="",