修复多条测试用例未填写断言出现脚本报错问题

优化loguru打印日志功能。
This commit is contained in:
aaronchenyongzhi 2023-09-14 23:32:25 +08:00
parent 7a0e17b104
commit 8adf8e964a
25 changed files with 171 additions and 2704 deletions

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,7 @@ from common.bif_functions import logger
__all__ = ['get_current_date', 'get_current_time', 'get_delta_time']
@logger.log_decorator()
@logger.catch
def get_current_date(fmt="%Y-%m-%d"):
"""
获取当前日期默认格式为%Y-%m-%d
@ -27,7 +27,7 @@ def get_current_date(fmt="%Y-%m-%d"):
return datetime.now().strftime(fmt)
@logger.log_decorator()
@logger.catch
def get_current_time(fmt="%Y-%m-%d %H:%M:%S"):
"""
获取当前时间默认格式为%Y-%m-%d %H:%M:%S
@ -40,7 +40,7 @@ def get_current_time(fmt="%Y-%m-%d %H:%M:%S"):
return datetime.now().strftime(fmt)
@logger.log_decorator()
@logger.catch
def get_delta_time(days=0, hours=0, minutes=0, seconds=0, fmt="%Y-%m-%d %H:%M:%S"):
"""
获取当前时间指定间隔后的时间

View File

@ -14,7 +14,7 @@ __all__ = ['md5_encryption']
from common.bif_functions import logger
@logger.log_decorator()
@logger.catch
def md5_encryption(raw_str, sha_str='', toupper=False):
"""
执行md5加密

View File

@ -14,7 +14,7 @@ from common.bif_functions import logger
__all__ = ['json_dumps', 'json_loads']
@logger.log_decorator()
@logger.catch
def json_dumps(obj):
"""
Serialize ``obj`` to a JSON formatted ``str``.
@ -27,7 +27,7 @@ def json_dumps(obj):
return json.dumps(obj, ensure_ascii=False)
@logger.log_decorator()
@logger.catch
def json_loads(obj):
"""
Deserialize ``obj`` (a ``str``, ``bytes`` or ``bytearray`` instance containing a JSON document) to a Python object.

View File

@ -12,7 +12,7 @@ from common.bif_functions import logger
__all__ = ['list_slice', 'sublist']
@logger.log_decorator()
@logger.catch
def list_slice(obj, index=None, start=None, end=None, step=1):
"""
切片方法
@ -37,7 +37,7 @@ def list_slice(obj, index=None, start=None, end=None, step=1):
return None
@logger.log_decorator()
@logger.catch
def sublist(raw_list, start=None, end=None):
"""
截取子列表

View File

@ -16,7 +16,7 @@ from common.bif_functions import logger
__all__ = ['random_choice', 'gen_random_num', 'gen_random_str', 'random_gps']
@logger.log_decorator()
@logger.catch
def random_choice(args):
"""
随机选择
@ -29,7 +29,7 @@ def random_choice(args):
return random.choice(args)
@logger.log_decorator()
@logger.catch
def gen_random_num(length):
"""
随机生成指定长度的数字
@ -42,7 +42,7 @@ def gen_random_num(length):
return random.randint(int('1' + '0' * (int(length) - 1)), int('9' * int(length)))
@logger.log_decorator()
@logger.catch
def gen_random_str(length):
"""
生成指定长度的随机字符串

View File

@ -15,7 +15,7 @@ from common.bif_functions import logger
__all__ = ['regex_extract']
@logger.log_decorator()
@logger.catch
def regex_extract(string, pattern, group=None):
"""
根据正则表达式提取内容

View File

@ -31,7 +31,7 @@ def substr(raw_str, start=None, end=None):
return ''
@logger.log_decorator()
@logger.catch
def str_join(obj, connector=","):
"""
连接任意数量的字符

View File

@ -14,7 +14,7 @@ from common.bif_functions import logger
__all__ = ['get_timestamp', 'ms_fmt_hms']
@logger.log_decorator("错误原因时间戳的长度只能在10到16位之间默认返回长度为13位的时间戳")
@logger.catch
def get_timestamp(length=13):
"""
获取时间戳字符串长度最多为16位默认13位
@ -34,7 +34,7 @@ def get_timestamp(length=13):
get_timestamp(13)
@logger.log_decorator()
@logger.catch
def ms_fmt_hms(ms):
"""
将毫秒转换成 h:m:s.ms格式字符串

View File

@ -1,65 +1,66 @@
from functools import wraps
def _create_test_name(index, name, title):
"""
Create a new test name based on index and name.
:param index: Index for generating the test name.
:param name: Base name for the test.
:param title: Base title for the test.
:return: Generated test name.
"""
test_name = f"{name}_{index + 1:03}_{title}"
return test_name
def _create_test_name(index,name, title):
"""
Create a new test name based on index and name.
:param index: Index for generating the test name.
:param name: Base name for the test.
:param title: Base title for the test.
:return: Generated test name.
"""
test_name = f"{name}_{index + 1:03}_{title}"
return test_name
def _set_function_attributes(func, original_func, new_name, test_desc):
"""
Set attributes of a function.
:param func: The function to set attributes for.
:param original_func: The original function being wrapped.
:param new_name: New name for the function.
:param test_desc: New documentation for the function.
"""
func.__wrapped__ = original_func
func.__name__ = new_name
func.__doc__ = test_desc
"""
Set attributes of a function.
:param func: The function to set attributes for.
:param original_func: The original function being wrapped.
:param new_name: New name for the function.
:param test_desc: New documentation for the function.
"""
func.__wrapped__ = original_func
func.__name__ = new_name
func.__doc__ = test_desc
def _update_func(new_func_name, params, test_desc, func, *args, **kwargs):
"""
Create a wrapper function with updated attributes.
:param new_func_name: New name for the wrapper function.
:param params: Test parameters.
:param test_desc: Test description.
:param func: Original function to be wrapped.
:param args: Additional positional arguments for the function.
:param kwargs: Additional keyword arguments for the function.
:return: Wrapped function.
"""
@wraps(func)
def wrapper(self):
return func(self, params, *args, **kwargs)
_set_function_attributes(wrapper, func, new_func_name, test_desc)
return wrapper
"""
Create a wrapper function with updated attributes.
:param new_func_name: New name for the wrapper function.
:param params: Test parameters.
:param test_desc: Test description.
:param func: Original function to be wrapped.
:param args: Additional positional arguments for the function.
:param kwargs: Additional keyword arguments for the function.
:return: Wrapped function.
"""
@wraps(func)
def wrapper(self):
return func(self, params, *args, **kwargs)
_set_function_attributes(wrapper, func, new_func_name, test_desc)
return wrapper
def ddt(cls):
"""
:param cls: 测试类
:return:
"""
for func_name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
name = str(case_data.get("Name", "缺少Name"))
test_desc = str(case_data.get("Description", "缺少Description"))
new_test_name = _create_test_name(index, func_name, name)
func2 = _update_func(new_test_name, case_data, test_desc, func)
setattr(cls, new_test_name, func2)
else:
# Avoid name clashes
delattr(cls, func_name)
return cls
"""
:param cls: 测试类
:return:
"""
for func_name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
name = str(case_data.get("Name", "缺少Name"))
test_desc = str(case_data.get("Description", "缺少Description"))
# new_test_name = _create_test_name(name)
new_test_name = _create_test_name(index, func_name, name)
func2 = _update_func(new_test_name, case_data, test_desc, func)
setattr(cls, new_test_name, func2)
else:
# Avoid name clashes
delattr(cls, func_name)
return cls

View File

@ -28,7 +28,7 @@ class DataExtractor(Environments):
def __init__(self):
super().__init__()
@logger.log_decorator("提取参数出现了意想不到的错误!!")
@logger.catch
def substitute_data(self, response, regex=None, keys=None, deps=None, jp_dict=None):
"""
数据提取

View File

@ -57,7 +57,7 @@ class DoExcel:
sub_data[FieldNames.SHEET] = sheet_name
yield sub_data
@logger.log_decorator()
@logger.catch
def write_back(self, sheet_name, i, **kwargs):
"""
@ -78,7 +78,7 @@ class DoExcel:
sheet.cell(i + 1, 26).value = assertions
self.wb.save(self.file_name)
@logger.log_decorator()
@logger.catch
def clear_date(self):
"""
执行清空单元格数据
@ -97,7 +97,7 @@ class DoExcel:
self.wb.save(self.file_name)
return f"清空指定 {sheets} 中的单元格成功"
@logger.log_decorator()
@logger.catch
def get_excel_init(self):
"""
获取 excel sheet 名称为 init 中的基础数据

View File

@ -128,7 +128,7 @@ class DoExcel:
row_data.append(cell_value)
return row_data
@logger.log_decorator()
@logger.catch
def save(self, filename=None):
"""
获取文件名
@ -145,7 +145,7 @@ class DoExcel:
# print("保存成功")
return True
@logger.log_decorator()
@logger.catch
def do_main(self, output_filename=None, *data):
"""
动态保存列表嵌套字典的数据到 excel

View File

@ -6,116 +6,82 @@
@desc: 日志封装
"""
import os
from functools import wraps
from time import perf_counter
from loguru import logger
from common.utils.decorators import singleton
from config.config import Config
LOG_DIR = Config.LOG_PATH
@singleton
class MyLogger:
"""
根据时间文件大小切割日志
"""
def __init__(self, log_dir=LOG_DIR, max_size=20, retention='7 days'):
self.log_dir = log_dir
self.max_size = max_size
self.retention = retention
self.logger = self.configure_logger()
def configure_logger(self):
"""
Returns:
"""
# 创建日志目录
os.makedirs(self.log_dir, exist_ok=True)
shared_config = {
"level": "ERROR",
"enqueue": True,
"backtrace": False,
"format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
}
# 添加按照日期和大小切割的文件 handler
logger.add(
sink=f"{self.log_dir}/{{time:YYYY-MM-DD}}.log",
rotation=f"{self.max_size} MB",
retention=self.retention,
**shared_config
)
# 配置按照等级划分的文件 handler 和控制台输出
logger.add(sink=self.get_log_path, **shared_config)
return logger
def get_log_path(self, message: str) -> str:
"""
根据等级返回日志路径
Args:
message:
Returns:
"""
log_level = message.record["level"].name.lower()
log_file = f"{log_level}.log"
log_path = os.path.join(self.log_dir, log_file)
return log_path
def __getattr__(self, level: str):
return getattr(self.logger, level)
def log_decorator(self, msg="快看,异常了,别唧唧哇哇,快排查!!"):
"""
日志装饰器记录函数的名称参数返回值运行时间和异常信息
Args:
logger: 日志记录器对象
Returns:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# self.logger.info(f'-----------分割线-----------')
# self.logger.info(f'| 调用函数: {func.__name__} | args: {args} kwargs:{kwargs}')
start = perf_counter() # 开始时间
try:
result = func(*args, **kwargs)
end = perf_counter() # 结束时间
duration = end - start
# self.logger.info(f"| 结束调用函数: {func.__name__}, duration{duration:4f}s")
return result
except Exception as e:
self.logger.error(f"| called {func.__name__} | error: {msg}: {e}")
# self.logger.info(f"-----------分割线-----------")
return wrapper
return decorator
"""
根据时间文件大小以及日志等级切割日志
"""
def __init__(self, log_dir=Config.LOG_PATH):
self.log_dir = log_dir
self.logger = self.configure_logger()
def configure_logger(self):
"""
Returns:
"""
# 创建日志目录
os.makedirs(self.log_dir, exist_ok=True)
shared_config = {
"level": "DEBUG",
"enqueue": True,
"backtrace": False,
"encoding": "utf-8",
"rotation": "50 MB",
"retention": "7 days",
"format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {file} | {module} | {message}",
}
# 添加按照等级划分以及日期和大小切割的文件 handler
for level in ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]:
logger.add(
sink=self.get_log_path(f"{level}.log"),
filter=self.level_filter(level), # 使用 level_filter 方法
**shared_config
)
return logger
def level_filter(self, level):
"""过滤日志等级"""
def is_level(record):
return record["level"].name == level
return is_level
def get_log_path(self, filename):
return os.path.join(self.log_dir, filename)
def trace(self, msg):
self.logger.trace(msg)
def __getattr__(self, level):
return getattr(self.logger, level)
def catch(self, *args, **kwargs):
return self.logger.catch(*args, **kwargs)
if __name__ == '__main__':
log = MyLogger()
@log.log_decorator("勇哥也不知道错在哪里")
def test_zero_division_error(a, b):
return a / b
for i in range(1000):
log.error('错误信息')
log.critical('严重错误信息')
test_zero_division_error(1, 0)
log.debug('调试信息')
log.info('普通信息')
log.success('成功信息')
log.warning('警告信息')
log = MyLogger()
@log.catch
def test_zero_division_error(a, b):
return a / b
for i in range(100):
test_zero_division_error(1, 0)
log.debug('调试信息')
log.info('普通信息')
log.success('成功信息')
log.warning('警告信息')
log.error('错误信息')
log.critical('严重错误信息')

View File

@ -54,7 +54,10 @@ def request_retry_on_exception(retries=2, delay=1.5):
print(f"| 请求 body --> {response.request.body}")
print(f"| 接口状态--> {response.status_code}")
print(f"| 接口耗时--> {response.elapsed}")
print(f"| 接口响应--> {response.text}")
try:
print(f"| 接口响应--> {response.json()}")
except:
print(f"| 接口响应--> {response.text}")
except Exception as error:
print(f"| 第{i + 1}次请求参数=【{args}__{kwargs}")
# print(f"| 代码耗时--> {time.time() - st}")

View File

@ -119,7 +119,7 @@ class InvalidParameterFormatError(MyBaseException):
def __init__(self, parameter_info, reason):
msg = f"无效的参数格式异常parameter_info={parameter_info},原因={reason}"
super().__init__(msg)
self.logger.error(msg)
self.logger.warning(msg)
class ResponseJsonConversionError(MyBaseException):
@ -129,7 +129,7 @@ class ResponseJsonConversionError(MyBaseException):
def __init__(self, response_text, reason):
msg = f"响应内容转换为 JSON 格式异常:响应内容={response_text}, 原因={reason}"
super().__init__(msg)
self.logger.error(msg)
self.logger.warning(msg)
class DynamicLoadingError(MyBaseException):

View File

@ -18,7 +18,7 @@ class Loaders(HttpClient):
def __init__(self):
super().__init__()
@logger.log_decorator()
@logger.catch
def load_built_in_functions(self, model):
"""
加载bif_functions包中的内建方法
@ -31,7 +31,7 @@ class Loaders(HttpClient):
return built_in_functions
@staticmethod
@logger.log_decorator()
@logger.catch
def load_built_in_comparators() -> object:
"""
加载包中的内建比较器
@ -45,7 +45,7 @@ class Loaders(HttpClient):
return built_in_comparators
@logger.log_decorator()
@logger.catch
def set_bif_fun(self, model):
"""
加载内置方法

View File

@ -105,7 +105,7 @@ class Validator(Loaders):
"""
if not validate_variables:
self.assertions = '未填写预期结果默认断言HTTP请求状态码'
self.assertions = ['未填写预期结果默认断言HTTP请求状态码']
return
self.validate_variables_list.clear()
self.assertions.clear()

View File

@ -54,7 +54,7 @@ class Config:
"url": "https://oapi.dingtalk.com/robot/send?access_token=7d1e11079e00a4ca9f11283f526349abd5ba3f792ef7bcb346909ff215af02de",
"secret": "SEC441dbbdb8dbe150e5fc3e348bb449d3113b1be1a90be527b898ccd78c51566c1",
"key": "", # 安全关键字
"atMobiles": "xxxxx", # 需要@指定人员的手机号
"atMobiles": "1827813600", # 需要@指定人员的手机号
"isAtAll": True, # 是否@ 所有人
"except_info": False # 是否发送测试不通过的异常数据
}

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

28
run.py
View File

@ -12,23 +12,29 @@ sys.path.append("./")
sys.path.append('src')
from config.config import Config
from common.bif_functions.bif_datetime import get_current_time
from common.core.testRunner import TestRunner
from common.utils.decorators import install_dependencies
@install_dependencies
def run():
test_report = Config.TEST_REPORT
test_case = unittest.defaultTestLoader.discover(Config.SCRIPT, pattern="test_*.py")
runner = TestRunner(test_case, report_dir=test_report, title="接口自动化测试报告", templates=2, tester="kira",
desc="自动化测试")
runner.run()
# # get_failed_test_cases = runner.get_failed_test_cases()
# 发送通知
# runner.email_notice()
# runner.dingtalk_notice()
# runner.weixin_notice()
test_report = Config.TEST_REPORT
test_case = unittest.defaultTestLoader.discover(Config.SCRIPT, pattern="test_*.py")
runner = TestRunner(test_case,
report_dir=test_report,
filename=Config.TEST_REPORT_FILE,
title="接口自动化测试报告",
templates=2,
tester="kira",
desc="自动化测试")
runner.run()
# # get_failed_test_cases = runner.get_failed_test_cases()
# 发送通知
# runner.email_notice()
runner.dingtalk_notice()
runner.weixin_notice()
if __name__ == '__main__':
run()
run()

Binary file not shown.

Binary file not shown.

View File

@ -298,7 +298,7 @@
{% endfor %}
</select>
</th>
<th scope="col" style="width: 15%;padding: 0">测试方法</th>
<th scope="col" style="width: 15%;padding: 0">测试模块</th>
<th scope="col" style="width: 20%;padding: 0">用例描述</th>
<th scope="col" style="width: 15%;padding: 0">执行时间</th>
<th scope="col" style="width: 20%;padding: 0">