pytest-api-framework-alpha 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- framework/__init__.py +0 -0
- framework/allure_report.py +35 -0
- framework/base_class.py +61 -0
- framework/conftest.py +523 -0
- framework/db/__init__.py +0 -0
- framework/db/mysql_db.py +111 -0
- framework/db/redis_db.py +142 -0
- framework/exit_code.py +19 -0
- framework/extract.py +101 -0
- framework/global_attribute.py +100 -0
- framework/http_client.py +163 -0
- framework/render_data.py +185 -0
- framework/report.py +102 -0
- framework/startapp.py +126 -0
- framework/utils/__init__.py +0 -0
- framework/utils/common.py +211 -0
- framework/utils/encrypt.py +387 -0
- framework/utils/log_util.py +3 -0
- framework/utils/teams_util.py +48 -0
- framework/utils/yaml_util.py +25 -0
- framework/validate.py +207 -0
- pytest_api_framework_alpha-0.1.0.dist-info/METADATA +29 -0
- pytest_api_framework_alpha-0.1.0.dist-info/RECORD +25 -0
- pytest_api_framework_alpha-0.1.0.dist-info/WHEEL +5 -0
- pytest_api_framework_alpha-0.1.0.dist-info/top_level.txt +1 -0
framework/db/mysql_db.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import pymysql
|
|
2
|
+
from dbutils.pooled_db import PooledDB
|
|
3
|
+
|
|
4
|
+
from framework.utils.log_util import logger
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MysqlDB(object):
|
|
11
|
+
|
|
12
|
+
def __init__(self, host, username, password, port, db):
|
|
13
|
+
|
|
14
|
+
self.pool = PooledDB(
|
|
15
|
+
creator=pymysql,
|
|
16
|
+
maxconnections=1,
|
|
17
|
+
mincached=1,
|
|
18
|
+
maxcached=1,
|
|
19
|
+
blocking=True,
|
|
20
|
+
maxusage=None,
|
|
21
|
+
setsession=[],
|
|
22
|
+
ping=0,
|
|
23
|
+
host=host,
|
|
24
|
+
user=username,
|
|
25
|
+
password=password,
|
|
26
|
+
db=db,
|
|
27
|
+
port=port,
|
|
28
|
+
charset='utf8'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def query(self, sql):
|
|
32
|
+
"""查询,返回结果"""
|
|
33
|
+
connection = self.pool.connection() # 获取一个连接
|
|
34
|
+
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
|
35
|
+
try:
|
|
36
|
+
cursor.execute(sql)
|
|
37
|
+
logger.info(f"执行SQL: {sql}")
|
|
38
|
+
result = cursor.fetchall()
|
|
39
|
+
if len(result) == 1:
|
|
40
|
+
result = result[0]
|
|
41
|
+
elif len(result) == 0:
|
|
42
|
+
result = None
|
|
43
|
+
if isinstance(result, dict) or isinstance(result, list):
|
|
44
|
+
logger.info(f"SQL执行结果: {json.dumps(result, default=MysqlDB.custom_serializer)}")
|
|
45
|
+
else:
|
|
46
|
+
logger.info(f"SQL执行结果: {result}")
|
|
47
|
+
return result
|
|
48
|
+
except pymysql.MySQLError as e:
|
|
49
|
+
logger.error(f"Error executing query: {e}")
|
|
50
|
+
return None
|
|
51
|
+
finally:
|
|
52
|
+
cursor.close() # 关闭游标
|
|
53
|
+
connection.close() # 将连接返回到连接池
|
|
54
|
+
|
|
55
|
+
def insert(self, sql):
|
|
56
|
+
"""修改,新增,删除"""
|
|
57
|
+
connection = self.pool.connection() # 获取一个连接
|
|
58
|
+
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
|
59
|
+
try:
|
|
60
|
+
result = cursor.execute(sql)
|
|
61
|
+
connection.commit()
|
|
62
|
+
inserted_id = cursor.lastrowid
|
|
63
|
+
print(f"插入的记录的主键ID: {inserted_id}")
|
|
64
|
+
return inserted_id
|
|
65
|
+
except pymysql.MySQLError as e:
|
|
66
|
+
print(e)
|
|
67
|
+
return None
|
|
68
|
+
finally:
|
|
69
|
+
cursor.close() # 关闭游标
|
|
70
|
+
connection.close() # 将连接返回到连接池
|
|
71
|
+
|
|
72
|
+
def execute(self, sql):
|
|
73
|
+
"""修改,新增,删除"""
|
|
74
|
+
connection = self.pool.connection() # 获取一个连接
|
|
75
|
+
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
|
76
|
+
try:
|
|
77
|
+
result = cursor.execute(sql)
|
|
78
|
+
logger.info(f"执行SQL: {sql}")
|
|
79
|
+
connection.commit()
|
|
80
|
+
logger.info(f"SQL执行结果: {result}")
|
|
81
|
+
return result
|
|
82
|
+
except pymysql.MySQLError as e:
|
|
83
|
+
logger.error(f"Error executing execute: {e}")
|
|
84
|
+
return None
|
|
85
|
+
finally:
|
|
86
|
+
cursor.close() # 关闭游标
|
|
87
|
+
connection.close() # 将连接返回到连接池
|
|
88
|
+
|
|
89
|
+
def executemany(self, sql, data):
|
|
90
|
+
"""修改,新增,删除"""
|
|
91
|
+
connection = self.pool.connection() # 获取一个连接
|
|
92
|
+
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
|
93
|
+
try:
|
|
94
|
+
cursor.executemany(sql, data)
|
|
95
|
+
logger.info(f"执行SQL: {sql}")
|
|
96
|
+
connection.commit()
|
|
97
|
+
logger.info(f"{cursor.rowcount} records inserted.")
|
|
98
|
+
except pymysql.MySQLError as e:
|
|
99
|
+
logger.error(f"Error executing execute: {e}")
|
|
100
|
+
return None
|
|
101
|
+
finally:
|
|
102
|
+
cursor.close() # 关闭游标
|
|
103
|
+
connection.close() # 将连接返回到连接池
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def custom_serializer(obj):
|
|
107
|
+
if isinstance(obj, datetime):
|
|
108
|
+
return obj.strftime('%Y-%m-%d %H:%M:%S') # 转换时间格式
|
|
109
|
+
elif isinstance(obj, Decimal):
|
|
110
|
+
return float(obj) # 转换 Decimal 为 float
|
|
111
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
framework/db/redis_db.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from redis import Redis
|
|
3
|
+
from redis.exceptions import RedisError
|
|
4
|
+
|
|
5
|
+
from framework.utils.log_util import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RedisDB(Redis):
|
|
9
|
+
|
|
10
|
+
def __init__(self, host, port, password, db, max_connections=10):
|
|
11
|
+
super(RedisDB, self).__init__()
|
|
12
|
+
self.db = db
|
|
13
|
+
self.max_connections = max_connections
|
|
14
|
+
self.conn = Redis(
|
|
15
|
+
host=host,
|
|
16
|
+
port=port,
|
|
17
|
+
password=password,
|
|
18
|
+
db=self.db,
|
|
19
|
+
decode_responses=True
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __del__(self):
|
|
23
|
+
try:
|
|
24
|
+
self.conn.close()
|
|
25
|
+
except RedisError as e:
|
|
26
|
+
logger.info(e)
|
|
27
|
+
raise RedisError(f"redis close error>>>{e}")
|
|
28
|
+
|
|
29
|
+
def set_string(self, name, value, ex=None):
|
|
30
|
+
if ex:
|
|
31
|
+
res = self.conn.set(name, value, ex)
|
|
32
|
+
logger.info(f'set {name} {value} {ex} --->{res}')
|
|
33
|
+
else:
|
|
34
|
+
res = self.conn.set(name, value)
|
|
35
|
+
logger.info(f'set {name} {value} --->{res}')
|
|
36
|
+
if res:
|
|
37
|
+
return res
|
|
38
|
+
else:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def set_hash(self, name, value):
|
|
42
|
+
res = self.conn.hmset(name, value)
|
|
43
|
+
logger.info(f'hmset {name} {value} --->{res}')
|
|
44
|
+
if res:
|
|
45
|
+
return res
|
|
46
|
+
else:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def set_list(self, name, value):
|
|
50
|
+
"""
|
|
51
|
+
value可以是字典;列表[字典];列表[数字];列表[字符串];空字典;空列表;空字符串;None
|
|
52
|
+
:param name:
|
|
53
|
+
:param value:
|
|
54
|
+
:return:
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if value:
|
|
58
|
+
if isinstance(value, dict):
|
|
59
|
+
self.push(name, value)
|
|
60
|
+
elif isinstance(value, list):
|
|
61
|
+
if isinstance(value[0], dict):
|
|
62
|
+
value = [json.dumps(item, ensure_ascii=False) for item in value]
|
|
63
|
+
self.conn.lpush(name, *value)
|
|
64
|
+
elif isinstance(value[0], (int, str)):
|
|
65
|
+
self.conn.lpush(name, json.dumps(value, ensure_ascii=False))
|
|
66
|
+
else:
|
|
67
|
+
self.conn.lpush(name, str(value))
|
|
68
|
+
else:
|
|
69
|
+
self.conn.lpush(name, value)
|
|
70
|
+
else:
|
|
71
|
+
self.conn.lpush(name, json.dumps(value))
|
|
72
|
+
|
|
73
|
+
def get_string(self, name):
|
|
74
|
+
res = self.conn.get(name)
|
|
75
|
+
logger.info(f'get {name}')
|
|
76
|
+
if res:
|
|
77
|
+
return res
|
|
78
|
+
else:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def get_hash(self, name):
|
|
82
|
+
res = self.conn.hgetall(name)
|
|
83
|
+
logger.info(f'hgetall {name}')
|
|
84
|
+
if res:
|
|
85
|
+
return res
|
|
86
|
+
else:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def get_list(self, name):
|
|
90
|
+
res = self.conn.lrange(name, 0, -1)
|
|
91
|
+
if res:
|
|
92
|
+
return [json.loads(item) for item in res]
|
|
93
|
+
|
|
94
|
+
def get_ttl(self, name):
|
|
95
|
+
res = self.conn.ttl(name)
|
|
96
|
+
logger.info(f'ttl {name} ---> {res}')
|
|
97
|
+
if res:
|
|
98
|
+
return res
|
|
99
|
+
else:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def set_ttl(self, name, time):
|
|
103
|
+
res = self.conn.expire(name, time)
|
|
104
|
+
logger.info(f'expire {name} {time} ---> {res}')
|
|
105
|
+
if res:
|
|
106
|
+
return res
|
|
107
|
+
else:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def exists(self, name):
|
|
111
|
+
res = self.conn.exists(name)
|
|
112
|
+
logger.info(f'exists {name} ---> {res}')
|
|
113
|
+
if res:
|
|
114
|
+
return True
|
|
115
|
+
else:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def delete(self, name):
|
|
119
|
+
res = self.conn.delete(name)
|
|
120
|
+
logger.info(f'delete {name} ---> {res}')
|
|
121
|
+
if res:
|
|
122
|
+
return True
|
|
123
|
+
else:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def push(self, name, value):
|
|
127
|
+
"""
|
|
128
|
+
通过list实现队列,从左侧推消息,消息体只能字典
|
|
129
|
+
:param name:
|
|
130
|
+
:param value:
|
|
131
|
+
:return:
|
|
132
|
+
"""
|
|
133
|
+
res = self.conn.lpush(name, json.dumps(value, ensure_ascii=False))
|
|
134
|
+
logger.info(f'lpush {name} ---> {value}')
|
|
135
|
+
if res:
|
|
136
|
+
return True
|
|
137
|
+
else:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == '__main__':
|
|
142
|
+
redis = RedisDB(db=0)
|
framework/exit_code.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExitCode(Enum):
|
|
5
|
+
ENV_NOT_EXIST = 10001 # 测试环境不存在
|
|
6
|
+
APP_NOT_EXIST = 10002 # 测试系统不存在
|
|
7
|
+
CONTEXT_YAML_NOT_EXIST = 10003 # 未获取到全局配置文件
|
|
8
|
+
CONTEXT_YAML_DATA_FORMAT_ERROR = 10004 # 全局配置文件格式不正确
|
|
9
|
+
CASE_YAML_NOT_EXIST = 10005 # 未获取到对应的YAML测试数据文件
|
|
10
|
+
CASE_DATA_NOT_EXIST = 10006 # 未获取到用例对应测试数据
|
|
11
|
+
GLOBAL_ATTRIBUTE_NOT_EXIST = 10007 # 未从CONTEXT中获取到对应变量
|
|
12
|
+
FUNCTION_NOT_EXIST = 10008 # 未从common.py中或faker对象中获取到对应函数
|
|
13
|
+
EXTRACT_KEY_NOT_EXIST = 10009 # 未从响应中提取到指定变量
|
|
14
|
+
LOGIN_ERROR = 10010 # 系统登录失败
|
|
15
|
+
YAML_MISSING_FIELDS = 10011 # 缺少必填字段
|
|
16
|
+
MISSING_ASSERTIONS = 10012 # 缺少断言
|
|
17
|
+
APP_OR_ACCOUNT_NOT_EXIST = 10013 # 账号角色不存在
|
|
18
|
+
MORE_THAN_ONE_TEST_SUITE_SETUP = 10014 # test_suite_setup标签只能配置一个
|
|
19
|
+
LOAD_DATABASE_INFO_ERROR = 10015
|
framework/extract.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import traceback
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import allure
|
|
6
|
+
import pytest
|
|
7
|
+
from box import Box
|
|
8
|
+
from jsonpath import jsonpath
|
|
9
|
+
|
|
10
|
+
from framework.utils.common import is_digit
|
|
11
|
+
from framework.utils.log_util import logger
|
|
12
|
+
from framework.exit_code import ExitCode
|
|
13
|
+
from framework.global_attribute import CONTEXT
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Extract(object):
|
|
17
|
+
def __init__(self, response, belong_app):
|
|
18
|
+
self.response = response
|
|
19
|
+
self.belong_app = belong_app
|
|
20
|
+
self.context = CONTEXT
|
|
21
|
+
|
|
22
|
+
def extracts(self, expressions: List[dict]):
|
|
23
|
+
for item in expressions:
|
|
24
|
+
key = list(item.keys())[0].strip()
|
|
25
|
+
expression = item.get(key).strip()
|
|
26
|
+
try:
|
|
27
|
+
# jsonpath表达式
|
|
28
|
+
if expression.lower().startswith("$."):
|
|
29
|
+
self.extract_by_jsonpath(key, expression)
|
|
30
|
+
# 正则表达式
|
|
31
|
+
elif expression.startswith("/") and expression.endswith("/"):
|
|
32
|
+
self.extract_by_regex(key, expression[1: -1])
|
|
33
|
+
# box句点表达式
|
|
34
|
+
else:
|
|
35
|
+
self.extract_by_box(key, expression)
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
with allure.step(f"后置提取变量{key}失败, 失败原因: {e}"):
|
|
39
|
+
logger.error(f"后置提取变量{key}失败, 失败原因: {e}")
|
|
40
|
+
traceback.print_exc()
|
|
41
|
+
pytest.exit(ExitCode.EXTRACT_KEY_NOT_EXIST)
|
|
42
|
+
|
|
43
|
+
def extract(self, key, expression):
|
|
44
|
+
return self.extracts([{key.strip(): expression.strip()}])
|
|
45
|
+
|
|
46
|
+
def extract_by_jsonpath(self, key, expression):
|
|
47
|
+
try:
|
|
48
|
+
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
49
|
+
self.belong_app, key = key.split(".", 1)
|
|
50
|
+
|
|
51
|
+
extract_value = jsonpath(self.response.json(), expression)[0]
|
|
52
|
+
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
|
53
|
+
self.context.set(key, extract_value, self.belong_app)
|
|
54
|
+
logger.info(f"后置提取变量: {key}: {extract_value}")
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response.json()}")
|
|
58
|
+
raise Exception(f"jsonpath表达式错误或响应内容异常{e} 表达式: {expression};响应内容: {self.response.json()}")
|
|
59
|
+
|
|
60
|
+
def extract_by_regex(self, key, reg_expression):
|
|
61
|
+
try:
|
|
62
|
+
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
63
|
+
self.belong_app, key = key.split(".", 1)
|
|
64
|
+
extract_value = re.search(reg_expression, self.response.text, flags=re.S).group()
|
|
65
|
+
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
|
66
|
+
if is_digit(extract_value):
|
|
67
|
+
self.context.set(key, eval(extract_value), self.belong_app)
|
|
68
|
+
else:
|
|
69
|
+
self.context.set(key, extract_value, self.belong_app)
|
|
70
|
+
logger.info(f"后置提取变量: {key}: {extract_value}")
|
|
71
|
+
|
|
72
|
+
except Exception:
|
|
73
|
+
logger.error(f"正则表达式或响应内容异常。表达式: {reg_expression}; 响应内容: {self.response.text}")
|
|
74
|
+
raise Exception(f"正则表达式或响应内容异常。表达式: {reg_expression}; 响应内容: {self.response.text}")
|
|
75
|
+
|
|
76
|
+
def extract_by_box(self, key, expression):
|
|
77
|
+
if key.startswith(tuple(f"{app}." for app in self.context.all_app)):
|
|
78
|
+
self.belong_app, key = key.split(".", 1)
|
|
79
|
+
extract_value = self.get_nested_value(Box(self.response.json()), expression)
|
|
80
|
+
if not extract_value:
|
|
81
|
+
logger.error(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response.json()}")
|
|
82
|
+
raise Exception(f"box表达式或响应内容异常 表达式: {expression}; 响应内容: {self.response.json()}")
|
|
83
|
+
else:
|
|
84
|
+
with allure.step(f"后置提取变量: {key}: {extract_value}"):
|
|
85
|
+
self.context.set(key, extract_value, self.belong_app)
|
|
86
|
+
logger.info(f"后置提取变量: {key}: {extract_value}")
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def get_nested_value(obj, attr_path):
|
|
90
|
+
"""通过字符串路径(如 'a.b[0].c')获取嵌套属性值"""
|
|
91
|
+
# 使用正则表达式分解路径,支持属性和索引的组合
|
|
92
|
+
path_elements = re.findall(r'(\w+)|\[(\d+)]', attr_path)
|
|
93
|
+
try:
|
|
94
|
+
for attr, index in path_elements:
|
|
95
|
+
if attr: # 属性部分
|
|
96
|
+
obj = getattr(obj, attr)
|
|
97
|
+
if index: # 索引部分
|
|
98
|
+
obj = obj[int(index)]
|
|
99
|
+
return obj
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from box import Box
|
|
6
|
+
from box.exceptions import BoxError
|
|
7
|
+
|
|
8
|
+
from framework.utils.log_util import logger
|
|
9
|
+
from framework.utils.common import singleton
|
|
10
|
+
from config.settings import ROOT_DIR
|
|
11
|
+
from framework.exit_code import ExitCode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GlobalAttribute(object):
|
|
15
|
+
def __setattr__(self, key, value):
|
|
16
|
+
super().__setattr__(
|
|
17
|
+
key,
|
|
18
|
+
Box(value) if isinstance(value, dict) else self.list2box(value) if isinstance(value, list) else value
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return Box(self.__dict__).to_json(indent=2)
|
|
23
|
+
|
|
24
|
+
def get(self, key, app=None):
|
|
25
|
+
if app:
|
|
26
|
+
obj = getattr(self, app, None)
|
|
27
|
+
else:
|
|
28
|
+
obj = self
|
|
29
|
+
value = getattr(obj, key, None)
|
|
30
|
+
return Box(value) if isinstance(value, dict) else self.list2box(value) if isinstance(value, list) else value
|
|
31
|
+
|
|
32
|
+
def set(self, key, value, app=None):
|
|
33
|
+
if app:
|
|
34
|
+
key = f"{app}.{key}"
|
|
35
|
+
self.set_by_chain(key, value)
|
|
36
|
+
else:
|
|
37
|
+
setattr(self, key, value)
|
|
38
|
+
|
|
39
|
+
def set_by_chain(self, key_chain, value):
|
|
40
|
+
"""
|
|
41
|
+
链式格式的key进行set
|
|
42
|
+
:param key_chain:
|
|
43
|
+
:param value:
|
|
44
|
+
:return:
|
|
45
|
+
"""
|
|
46
|
+
keys = key_chain.split(".")
|
|
47
|
+
for key in keys[:-1]:
|
|
48
|
+
if not hasattr(self, key):
|
|
49
|
+
setattr(self, key, Box()) # 创建一个空对象属性
|
|
50
|
+
self = getattr(self, key)
|
|
51
|
+
setattr(self, keys[-1], value)
|
|
52
|
+
|
|
53
|
+
def set_from_dict(self, dic, app=None):
|
|
54
|
+
for k, v in dic.items():
|
|
55
|
+
self.set(k, v, app)
|
|
56
|
+
|
|
57
|
+
def init_test_case_data_dict(self):
|
|
58
|
+
new_dict = dict()
|
|
59
|
+
new_dict['test_case_datas'] = dict()
|
|
60
|
+
for k, v in new_dict.items():
|
|
61
|
+
self.set(k, v)
|
|
62
|
+
|
|
63
|
+
def set_from_yaml(self, filename, env, app=None):
|
|
64
|
+
try:
|
|
65
|
+
file = os.path.join(ROOT_DIR, filename)
|
|
66
|
+
if not os.path.exists(file):
|
|
67
|
+
logger.error(f"{file}文件不存在")
|
|
68
|
+
pytest.exit(ExitCode.CONTEXT_YAML_NOT_EXIST)
|
|
69
|
+
self.set_from_dict(dict(Box().from_yaml(filename=file).get(env)), app)
|
|
70
|
+
except BoxError as e:
|
|
71
|
+
logger.error(f"{file}文件内容不是字典类型:{e}")
|
|
72
|
+
traceback.print_exc()
|
|
73
|
+
pytest.exit(ExitCode.CONTEXT_YAML_DATA_FORMAT_ERROR)
|
|
74
|
+
|
|
75
|
+
def delete(self, key):
|
|
76
|
+
delattr(self, key)
|
|
77
|
+
|
|
78
|
+
def list2box(self, array: list):
|
|
79
|
+
for index, item in enumerate(array):
|
|
80
|
+
if isinstance(item, dict):
|
|
81
|
+
array[index] = Box(item)
|
|
82
|
+
elif isinstance(item, list):
|
|
83
|
+
array[index] = self.list2box(item)
|
|
84
|
+
return array
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@singleton
|
|
88
|
+
class Context(GlobalAttribute):
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@singleton
|
|
93
|
+
class Config(GlobalAttribute):
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# 创建管理变量的全局对象,用于存储临时变量
|
|
98
|
+
CONTEXT = Context()
|
|
99
|
+
# 创建配置内容管理的全局对象
|
|
100
|
+
CONFIG = Config()
|
framework/http_client.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
import allure
|
|
6
|
+
import requests
|
|
7
|
+
from box import Box
|
|
8
|
+
import json as built_json
|
|
9
|
+
from jsonpath import jsonpath as jp
|
|
10
|
+
from requests.models import Response
|
|
11
|
+
from framework.extract import Extract
|
|
12
|
+
from framework.utils.log_util import logger
|
|
13
|
+
from framework.global_attribute import CONTEXT
|
|
14
|
+
from requests_toolbelt import MultipartEncoder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResponseUtil(object):
|
|
18
|
+
def __init__(self, response: Response):
|
|
19
|
+
self.response = response
|
|
20
|
+
self.box = Box(response.json())
|
|
21
|
+
|
|
22
|
+
def __getattr__(self, name):
|
|
23
|
+
try:
|
|
24
|
+
return getattr(self.box, name)
|
|
25
|
+
except AttributeError:
|
|
26
|
+
return getattr(self.response, name)
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return self.text
|
|
30
|
+
|
|
31
|
+
def jsonpath(self, expr, headers=False):
|
|
32
|
+
"""
|
|
33
|
+
使用jsonpath语法从响应体中提取内容
|
|
34
|
+
@param expr: jsonpath语法
|
|
35
|
+
@param headers: 从响应头中提取内容
|
|
36
|
+
@return:
|
|
37
|
+
"""
|
|
38
|
+
result = jp(dict(self.response.headers), expr) if headers else jp(self.json(), expr)
|
|
39
|
+
if result is False:
|
|
40
|
+
return ""
|
|
41
|
+
if len(result) == 1:
|
|
42
|
+
return result[0]
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def jsonp(self):
|
|
47
|
+
"""返回json"""
|
|
48
|
+
jsonp_re = re.compile(r".*?\((?P<text>.*)\)")
|
|
49
|
+
return built_json.loads(jsonp_re.match(self.response.text).group("text"))
|
|
50
|
+
|
|
51
|
+
def json(self):
|
|
52
|
+
return self.response.json()
|
|
53
|
+
|
|
54
|
+
def extract(self, app, key, expression):
|
|
55
|
+
"""
|
|
56
|
+
进行变量提取
|
|
57
|
+
:param app:
|
|
58
|
+
:param key: 保存变量的名称
|
|
59
|
+
:param expression: 提取变量表达式
|
|
60
|
+
:return:
|
|
61
|
+
"""
|
|
62
|
+
Extract(self.response, app).extract(key, expression)
|
|
63
|
+
|
|
64
|
+
def extracts(self, app, expressions: List[dict]):
|
|
65
|
+
"""
|
|
66
|
+
进行变量提取
|
|
67
|
+
:param app:
|
|
68
|
+
:param expressions: 提取变量表达式
|
|
69
|
+
:return:
|
|
70
|
+
"""
|
|
71
|
+
Extract(self.response, app).extracts(expressions)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HttpClient(object):
|
|
75
|
+
|
|
76
|
+
def __init__(self, headers=None):
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@param headers: 请求头
|
|
80
|
+
@param cookies: cookies
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
self.headers = {
|
|
84
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
|
85
|
+
"Content-Type": "application/json"
|
|
86
|
+
}
|
|
87
|
+
self.headers.update(headers or {})
|
|
88
|
+
self.response = None
|
|
89
|
+
|
|
90
|
+
def __send_request(self, data):
|
|
91
|
+
_is_multipart = data.get("_is_multipart")
|
|
92
|
+
request_obj = data.get("request")
|
|
93
|
+
with allure.step("发送请求"):
|
|
94
|
+
if _is_multipart:
|
|
95
|
+
m = MultipartEncoder(fields=request_obj["data"])
|
|
96
|
+
request_obj["data"] = m
|
|
97
|
+
request_obj["headers"] = {'Content-Type': m.content_type}
|
|
98
|
+
else:
|
|
99
|
+
request_obj["headers"].update(self.headers)
|
|
100
|
+
self.response = ResponseUtil(requests.request(**request_obj))
|
|
101
|
+
|
|
102
|
+
with allure.step(f"请求url: {request_obj.get('url')}"):
|
|
103
|
+
logger.info(f"请求url: {request_obj.get('url')}")
|
|
104
|
+
with allure.step(f"请求method: {request_obj.get('method')}"):
|
|
105
|
+
logger.info(f"请求method: {request_obj.get('method')}")
|
|
106
|
+
with allure.step(f"请求headers: {request_obj.get('headers')}"):
|
|
107
|
+
logger.info(f"请求headers: {request_obj.get('headers')}")
|
|
108
|
+
if request_obj.get('request'):
|
|
109
|
+
with allure.step(f"请求参数params: {request_obj.get('params')}"):
|
|
110
|
+
logger.info(f"请求参数params: {request_obj.get('params')}")
|
|
111
|
+
if request_obj.get('data'):
|
|
112
|
+
with allure.step(f"请求参数data: {request_obj.get('data')}"):
|
|
113
|
+
logger.info(f"请求参数data: {request_obj.get('data')}")
|
|
114
|
+
if request_obj.get('json'):
|
|
115
|
+
with allure.step(f"请求参数json: {request_obj.get('json')}"):
|
|
116
|
+
logger.info(f"请求参数json: {request_obj.get('json')}")
|
|
117
|
+
|
|
118
|
+
with allure.step("响应结果"):
|
|
119
|
+
with allure.step(f"响应状态码: {self.response.status_code}"):
|
|
120
|
+
logger.info(f"响应状态码: {self.response.status_code}")
|
|
121
|
+
with allure.step(f"响应headers: {self.response.headers}"):
|
|
122
|
+
logger.info(f"响应headers: {self.response.headers}")
|
|
123
|
+
with allure.step(f"响应body: {self.response.json()}"):
|
|
124
|
+
logger.info(f"响应body: {self.response.json()}")
|
|
125
|
+
|
|
126
|
+
# # 断言
|
|
127
|
+
# validates = params.get("validate")
|
|
128
|
+
# if validates:
|
|
129
|
+
# with allure.step("结果断言"):
|
|
130
|
+
# Validate(self.response).valid(validates)
|
|
131
|
+
|
|
132
|
+
# 提取变量
|
|
133
|
+
expressions = data.get("extract")
|
|
134
|
+
belong_app = data.get("_belong_app")
|
|
135
|
+
if expressions:
|
|
136
|
+
with allure.step("提取变量"):
|
|
137
|
+
Extract(self.response, belong_app).extracts(expressions)
|
|
138
|
+
|
|
139
|
+
return self.response
|
|
140
|
+
|
|
141
|
+
def request(self, data, **kwargs):
|
|
142
|
+
if data:
|
|
143
|
+
return self.__send_request(data)
|
|
144
|
+
|
|
145
|
+
return ResponseUtil(requests.request(headers=self.headers, **kwargs))
|
|
146
|
+
|
|
147
|
+
def post(self, app, url, data=None, json=None, **kwargs):
|
|
148
|
+
return self.request(method="post", url=urljoin(CONTEXT.get(app=app, key="domain"), url), data=data,
|
|
149
|
+
json=json, **kwargs)
|
|
150
|
+
|
|
151
|
+
def get(self, app, url, params=None, **kwargs):
|
|
152
|
+
return self.request(method="get", url=urljoin(CONTEXT.get(app=app, key="domain"), url), params=params,
|
|
153
|
+
**kwargs)
|
|
154
|
+
|
|
155
|
+
def put(self, app, url, data=None, **kwargs):
|
|
156
|
+
return self.request(method="put", url=urljoin(CONTEXT.get(app=app, key="domain"), url), data=data,
|
|
157
|
+
**kwargs)
|
|
158
|
+
|
|
159
|
+
def delete(self, app, url, **kwargs):
|
|
160
|
+
return self.request(method="delete", url=urljoin(CONTEXT.get(app=app, key="domain"), url), **kwargs)
|
|
161
|
+
|
|
162
|
+
def update_headers(self, headers):
|
|
163
|
+
self.headers.update(headers)
|