高效自动化测试框架-优秀实践01-日志

高效自动化测试框架-优秀实践01-日志

高效实践点

  1. 同一个用例的所有日志打印到同一个文件中

  2. 日志的归档按照用例的层级位置去归档

问题背景

  1. 日志打印零散,很难找到日志

  2. 有时候,会存在多个用例的日志直接打印在同一个文件中,查找即为困难

  3. 自动化框架在最开始的时候,如果测试无经验,极有可能直接使用print来打印所有日志,导致不能良好的归档

  4. 无具体的日志格式,导致排查脚本问题的时候,需要花费大量人力物理才能排查清楚

解决思路

  1. 所有测试脚本在执行前都要去注册一个日志器,并且在执行完脚本的时候,需要去接触注册

  2. 日志打印的格式需要统一,要包含时间,具体的文件,函数名,行数,以及测试主动抛出的日志

  3. 需要简洁,并且集成到测试框的核心中

相关代码

注册日志器和接触注册的代码

/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
​

如何集成到框架中

上面的例子中,还是直接显示的调用日志器的初始化和解除,即会编写很多重复的代码,并且和底层的代码未能做到解耦,一旦底层代码修改,那么将会需要很大人力物力去维护脚本

实际解决方法

  1. 在conftest中去初始化日志

  2. 使用fixture访问上下文的功能,获取到对应的测试类,然后用相关信息去出似乎日志器

  3. 使用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

你可能感兴趣的:(python,自动化,测试工具)