同一个用例的所有日志打印到同一个文件中
日志的归档按照用例的层级位置去归档
日志打印零散,很难找到日志
有时候,会存在多个用例的日志直接打印在同一个文件中,查找即为困难
自动化框架在最开始的时候,如果测试无经验,极有可能直接使用print来打印所有日志,导致不能良好的归档
无具体的日志格式,导致排查脚本问题的时候,需要花费大量人力物理才能排查清楚
所有测试脚本在执行前都要去注册一个日志器,并且在执行完脚本的时候,需要去接触注册
日志打印的格式需要统一,要包含时间,具体的文件,函数名,行数,以及测试主动抛出的日志
需要简洁,并且集成到测试框的核心中
注册日志器和接触注册的代码
/core/logger/init.py
# -*- coding:utf8 -*- import logging import logging.handlers import os import time from config.frame.basic import LOG_PATH class LoggerManager: def __init__(self): # 用于记录logger的配置信息 self.logger_info = dict() self.user_handler = None self.default_logger_name = "main" # 因为pytest串行执行时,只有一个进程线程,脚本执行前后需要注册解注册日志,多线程执行时,线程间独立 def get_log_file_name(self, case_file_path): new_folder_list = [] py_file_name = os.path.split(case_file_path)[1] py_file_name = py_file_name.strip(".py") current = time.localtime() log_file_name = py_file_name + "_%d_%d_%d_%d_%d_%d" % ( current.tm_year, current.tm_mon, current.tm_mday, current.tm_hour, current.tm_min, current.tm_sec ) + ".log" new_folder_list.append(py_file_name.strip(".py")) file_path = os.path.split(case_file_path)[0] cur_folder = "" while cur_folder != "cases": path_detail = os.path.split(file_path) file_path = path_detail[0] cur_folder = path_detail[1] new_folder_list.append(cur_folder) new_folder_list = new_folder_list[::-1] t_folder = "cases" for i in range(1, len(new_folder_list)): t_folder = os.path.join(t_folder, new_folder_list[i]) log_file_path = os.path.join(t_folder, log_file_name) log_file_path = os.path.join(LOG_PATH, log_file_path) print("get_log_file_name::所要输出日志文件的路径=>log_file_path", log_file_path) if not os.path.exists(os.path.dirname(log_file_path)): os.makedirs(os.path.dirname(log_file_path)) file = open(log_file_path, 'w') file.close() return log_file_path def register(self, case_file_path, console=True, default_level=logging.DEBUG, **kwargs): """ 注册logger :param logger_name: :param file_name: :param console: :param default_level: :param kwargs: :return: """ """ logger_info[logger_name] = dict(), 其中的key分别表示 timestamp: 表示创建的时间戳 file_path: 表示日志存储的路径 logger: 表示日志器 thread: 表示所属的线程 """ print("filename", case_file_path) filename = self.get_log_file_name(case_file_path) log_format = kwargs.get("format", None) if log_format is None: log_format = "%(asctime)s %(filename)s::%(module)s::%(funcName)s[%(lineno)d] %(levelname)s: %(message)s" # 获取新的loger实例 logger_name = self.default_logger_name logger = logging.getLogger(logger_name) self.logger_info[logger_name] = dict() self.logger_info[logger_name]["timestamp"] = time.localtime() # 如果设置了file_count, 则默认一个文件大小为1MB file_size_limit = kwargs.get("size_limit", 10*1024*1024) # 即一个日志文件最大10M file_max = kwargs.get("file_max", 6) file_mode = kwargs.get("mode", "w") if filename: self.logger_info[logger_name]["file_path"] = os.path.dirname(filename) file_handler = logging.handlers.RotatingFileHandler( filename=filename, mode=file_mode, maxBytes=file_size_limit, backupCount=file_max, encoding='utf-8' ) file_handler.setFormatter(logging.Formatter(fmt=log_format)) self.user_handler = file_handler logger.addHandler(file_handler) if console: stream_handler = logging.StreamHandler() stream_handler.setFormatter(logging.Formatter(fmt=log_format)) logger.addHandler(stream_handler) logger.setLevel(default_level) self.logger_info[logger_name]['logger'] = logger return logger def unregister(self, logger_name="main"): """ 删除注册的logger, 同时将需要打包的logger文件打包 :param logger_name: :return: """ print("logging.Logger.manager.loggerDict", logging.Logger.manager.loggerDict) print("logger_info", self.logger_info) if logger_name in logging.Logger.manager.loggerDict: logging.Logger.manager.loggerDict.pop(logger_name) # self.logger_info.pop(logger_name) # 因为如果在不同的地方初始化,那么这个信息并非是共享的,所以展示删除这行代码 def get_logger(self, logger_name="main"): return logging.getLogger(logger_name) # 因为有可能在多次初始化,日志管理器的类,所以先暂时直接返回 # if logger_name in self.logger_info: # return self.logger_info[logger_name]["logger"] # raise NameError(f"No log names {logger_name}") def logger_init(case_file_path): logger_mgt = LoggerManager() logger = logger_mgt.register(case_file_path) return logger def get_logger(): logger_mgt = LoggerManager() logger = logger_mgt.get_logger() return logger def logger_end(): logger_mgt = LoggerManager() logger_mgt.unregister()
在测试用例调用一些函数的时候,在函数在中,存在打日志的行为,下面的代码,提供获取日志器的函数
from . import get_logger logger = get_logger()
实际的函数调用实例
E:\Develop\LoranTest\cases\api\example\logger_template\test_logger.py
# -*- coding:utf8 -*- from core.logger import logger_init, logger_end from func_for_logger import for_logger_func_1 class TestLogger(object): def setup(self): print("__file__", __file__) self.logger = logger_init(__file__) self.logger.info("这是TestLogger测试用例的setup部分") pass def test_use_logger(self): self.logger.info("这是TestLogger测试用例的过程部分") for_logger_func_1() def teardown(self): logger_end()
对应的被调用的函数
E:\Develop\LoranTest\cases\api\example\logger_template\func_for_logger.py
# -*- coding:utf8 -*- from core.logger import get_logger def for_logger_func_1(): logger = get_logger() logger.info("这里是一条属于for_logger_func_1函数的日志") pass
上面的例子中,还是直接显示的调用日志器的初始化和解除,即会编写很多重复的代码,并且和底层的代码未能做到解耦,一旦底层代码修改,那么将会需要很大人力物力去维护脚本
实际解决方法
在conftest中去初始化日志
使用fixture访问上下文的功能,获取到对应的测试类,然后用相关信息去出似乎日志器
使用yield,先让测试脚本执行完,而后再去执行解除日志的步骤,即实现一种setup和teardown的功能
import pytest from core.logger import logger_init, logger_end @pytest.fixture(scope="class", autouse=True) def logger_fixture(request): print("\n=======================request start=================================") # print('测试方法的参数化数据:{}'.format(request.param)) # 此处需要结合@pytest.mark.parameter(indirect=Ture)来使用 print('测试方法所处模块的信息:{}'.format(request.module)) # print('测试方法信息:{}'.format(request.function)) # 此处有可能是因为装饰的是一个类,所以这里如函数的相关信息 print('测试方法所在的类的信息:{}'.format(request.cls)) print('测试方法所在路径信息:{}'.format(request.fspath)) print('测试方法调用的多个fixture函数(比如fixture函数之间的嵌套调用(包括pytest内嵌的fixture函数))信息:{}'.format(request.fixturenames)) print('测试方法调用的单个fixture函数(自己在程序中定义在测试方法中调用的fixture函数)信息:{}'.format(request.fixturename)) print('测试方法级别信息:{}'.format(request.scope)) print("\n=======================request end=================================") logger = logger_init(request.fspath) logger.info("用例初始化步骤前,先根据用例的__file__来实例化一个logger") yield logger.info("用例执行结束,自动调用logger_end来解除logger的注册,避免日志打印到多个文件中") logger_end() # 说明: 此处的拥有法即为在fixture中去访问上下文信息,是一个非常好的用法 # 参考地址: https://blog.csdn.net/mashang_z111/article/details/127112522
想要了解这个实践的全部代码,或者查看项目功能的其他代码,则访问我的github项目地址,本项目已经开源
GitHub - WaterLoran/LoranTest