第十章:调试与测试

第一节:断言与单元测试

断言

格式: assert +【条件表达式】
如果表达式成立,则程序能够正常向下执行,没有输出任何内容,否则抛出**AssertionError**
使用断言,可以简易迅速地对流程结果进行测试,预测结果是否相符;

例:

# 待测函数
def add(a, b):
    return a + b

# 使用断言进行测试:
assert add(3, 4) == 7

执行结果:
系统没有报错,断言是正确的,使用add这个函数得到的结果与预期相符

例:

# 待测函数
def add(a, b):
    return a * b

# 使用断言进行测试:
assert add(3, 4) == 7

这时系统抛出了AssertionError,说明断言是错误的,待测函数add的结果于预期并不一致

单元测试

所谓单元,指的是可测试对象的最小单位,通常指一个函数。单元测试只是对工程中的代码单元(通常细化到每个函数)进行正确性验证的工作;最常用的测试
对自己写的模块和类进行必要的单元测试,保证严谨正确,既是良好的开发习惯,也是一种规范;
Python标准库中单元测试的模块是 unittest

import unittest

测试用例

【要点】
测试用例类必须继承unittest.TestCase
具体的测试项函数必须以testxxx来命名
setUp()方法会在每个测试项执行前调用,如有必要的初始化工作可以通过覆写该方法来实现
tearDown()方法会在每个测试项结束后调用,如有善后工作可以通过覆写该方法来实现
在整个测试用例中,还有两个类,setUpClass(cls)tearDowwnClass(cls)也会在测试用例的执行前后各调用一次
在具体的测试项函数中,使用TestCase的assertXXX系列函数预言结果
如果程序执行结果与预测的一致,测该单项测试通过,否则不通过

【几个常用断言方法】
assertFalse(self,expr,msg) —— 断言正确
assertTrue(self,expr,msg) —— 断言错误
assertEqual(self,expr,msg) —— 断言相等

【注意】
测试与光标有关,当光标在某函数内时,执行的是此函数的测试,当想测试所有,需把光标放在unittest.main()区域

例:

import unittest

# 待测的工具类
class MathUtil:
    # a,b相加
    def sum(self, a, b):
        return a + b

    # a,b相减
    def sub(self, a, b):
        return a - b

    # 判断a是否大于b
    def gt(self, a, b):
        return a > b

# 测试用例类,必须继承于unittest.TestCase
class MathUtilTest(unittest.TestCase):
    '''MathUtil工具类测试用例'''

    # 测试项初始化方法
    def setUp(self):
        print("MathUtilTest setUp,测试项正在初始化...")
        # 创建实例(用于准备数据)
        self.mu = MathUtil()

    # 测试项结束时调用
    def tearDown(self):
    	del self.mu    # 消毁数据
        print("MathUtilTest tearDown,测试项已结束")

    '''
    一系列的测试方法,都必须以testxxx命名
    '''

    # 测试MathUtil的sum方法
    def testSum(self):
        print("正在测试Sum方法...")
        # 断言相等
        self.assertEqual(self.mu.sum(3, 4), 7,"Sum函数测试失败!!!")

    # 测试MathUtil的sub方法
    def testSub(self):
        print("正在测试Sub方法...")
        # 断言相等
        self.assertEqual(self.mu.sub(3, 4), 1, "Sub函数测试失败!!!")

    # 测试MathUtil的gt方法
    def testGt(self):
        print("正在测试Others方法...")
        # 断言真假
        self.assertTrue(self.mu.gt(5, 4))
        self.assertFalse(self.mu.gt(4, 5))

        # 断言抛出异常
        with self.assertRaises(TypeError):
            self.mu.sum(1, "2")


# 执行单元测试
if __name__ == '__main__':
    # 运行当前模块中的所有测试用例
    unittest.main()

正常执行结果:
在这里插入图片描述
非正常执行结果:
在这里插入图片描述

测试套件

测试套件,用于单独测试某个方法,而不必整个测式用例进行测试,相对灵活一些
unittest.main()方法会执行当前模块中的所有测试用例类中的所有测试项,这显得不太灵活;
unittest.TestSuite类是一个测试用例容器,可以按需添加测试用例于其中,使得单元测试既可以批量进行,又可以自主增减测试项目;

例:

import unittest
from unittest.runner import TextTestRunner

# 待测的工具类
class MathUtil:...

# 测试用例类,必须继承于unittest.TestCase
class MathUtilTest(unittest.TestCase):...
class MathUtilTest2(unittest.TestCase):...
class MathUtilTest2(unittest.TestCase):...

# 执行单元测试
if __name__ == '__main__':
    # 运行当前模块中的所有测试用例
    # unittest.main()
    
    # 定义一个测试套件
    suite = unittest.TestSuite()

    # 往测试套件里新增用例类下的所有测试项
    suite.addTest(unittest.makeSuite(MathUtilTest))
    suite.addTest(unittest.makeSuite(MathUtilTest2))

    # 执行测试套件
    runner = TextTestRunner()  # 执行器
    ret = runner.run(suite)
    print(ret)

第二节:文档测试与DEBUG

文档测试

文档指的就是Python模块,文档测试就是对一个py文件进行整体的测试,是一种简单粗暴的测试方式;
文档测试中的测试代码是以注释的形式写在文档中;
通过标准库API来触发文档测试:doctest.testmod(target_module)

例:有一个待测试的模块uut.py

'''
文档测试脚本

#预测加法的结果
>>> add(3,4)
7

#预测减法的结果
>>> sub(3,4)
-1

#预测幂的结果
>>> power(3,4)
81
'''

# 正确的加法函数
def add(a,b):
    return a + b

# 正确的加法函数
def sub(a,b):
    return a - b

# 错误的幂函数
def power(a,b):
    return a ** b - 1

开始对目标模块进行测试:

#引入文档测试模块
import doctest
# 引入要进行测试的目标模块uut
import uut

if __name__ == '__main__':
    #对uut进行文档测试
    doctest.testmod(uut)

测试结果:
在这里插入图片描述

DEBUG

【什么是DEBUG】
DEBUG是指对程序的执行过程进行逐行逐步调试
DEBUG时,程序从第一个断点处进入暂停状态,然后根据用户的指令,一步一步地进行执行,每执行一步,都能够从控制台中查看到程序和数据的所有细节;

【主要操作步骤】
在需要中止的地方打断点;
在IDE中右键选择“Debug XXX”;
按需【下一步(Step Over)】或【进入方法(Step Into) 】直到流程结束;

在分步执行的过程中,可以:
将重点怀疑的变量右击添加到观察(Add to watches);
调试过程中人为修改可疑变量的值(Set Value);
PS:DEBUG是一种效率不高的调试手段,它应用作程序调试的辅助手段而非主要手段;

例:
在这里插入图片描述

第三节:关于日志

什么是日志

网络设备、系统及服务程序等,在运作时都会产生一个叫log的事件记录;每一行日志都记载着日期、时间、使用者及动作等相关操作的描述。在软件项目工程中调试,不应该是DEBUG来进行的,应为检测的效率不高,应该以日志+单元测试为主要的方式。而日志往往都是后来用于回看的,通过回看日志可以发现当时发生了什么异常。Web通常把日志写到一个文件中

日志常用API

【全局日志logging】
basicConfig(level,format) —— 基本设置(级别,格式)
getLogger(name) —— 创建局部日志对象
Fomatter('%(asctime)s,%(name)s,%(levelname)s,%(message)s') —— 设置日志格式,name 指局部日志名
FileHandler("./logs/log.txt") —— 文件处理器,把日志传向文件
StreamHandler() —— 流处理器,把日志传向控制台
RotatingFileHandler("./logs/log3.txt",maxBytes=1*1024,backupCount=3) —— 回滚的文件处理器,传向动态存储上限的文件
logging.config.dictConfig(configDict) —— 字典设置,扩大了基本设置的局限性

【局部日志Logger】
info(msg) —— 打印信息
debug(msg) —— 打印调试信息
waming(msg) —— 打印敬告信息
error(msg,exc_info=True) —— 打印错误信息
setLevel(level) —— 设置上限等级
addHandler(handler) —— 设置处理器对象

【处理日志Handler】
setLevel(logging.INFO) —— 设置等级
setFormatter(formatter) —— 设置格式

日志的等级

Python源码对日志级别的是有定义的,数值越大,级别越高;
输出时,等于或高于配置级别的日志信息都会被输出;

【不同的级别的具体含义】
FATAL —— 致命错误
CRITICAL —— 特别糟糕的事情,如内存耗尽、磁盘空间为空,一般很少使用
ERROR —— 发生错误时,如IO操作失败或者连接问题
WARNING —— 发生很重要的事件,但是并不是错误时,如用户登录密码错误
INFO —— 处理请求或者状态变化等日常事务
DEBUG —— 调试过程中使用DEBUG等级,如算法中每个循环的中间状态
NOTSET —— 未设置

例:Python源码
在这里插入图片描述

日志的格式

%(levelno)s —— 打印日志级别的数值
%(levelname)s —— 打印日志级别的名称
%(pathname)s —— 打印当前执行程序的路径
%(filename)s —— 打印当前执行程序名
%(funcName)s —— 打印日志的当前函数
%(lineno)d —— 打印日志的当前行号
%(asctime)s —— 打印日志的时间
%(thread)d —— 打印线程ID
%(threadName)s —— 打印线程名称
%(process)d —— 打印进程ID
%(message)s —— 打印日志信息

向控制台输出日志

例1:输出的级别

import logging 

# 基本配置 
# level=logging.WARNING 输出WARNING以上的级别的日志内容 
# format定义了日志输出格式:'输出时间 - 日志名称 - 日志级别 - 日志内容
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 

# 打印日志 
logging.critical("大家好,我是critical,最高等级的错误信息")
logging.error("大家好,我是error,等级第二的严重错误信息")
logging.warning("大家好,warning,中等级的警告")
logging.info("大家好,我是INFO,初等级的提示错误")
logging.debug("大家好,我是debug,只是小人物,大家忽略我吧")

执行结果:
在这里插入图片描述

例2:局部日志

import logging

# 基本配置
#level=logging.DEBUG 输出DEBUG以上级别的日志内容
# format定义了日志输出格式:'输出时间 - 日志名称 - 日志级别 - 日志内容'
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 获取logger对象,命名为__name__,也可以命名为其他,如:mylog
logger = logging.getLogger(__name__)

#设置局部日志输出等级(输出所有等级)
logger.setLevel(logging.NOTSET)  # 完全没用,受限于根日志的设置等级

# 打印局部日志
logger.critical("大家好,我是critical,最高等级的错误信息")
logger.error("大家好,我是error,等级第二的严重错误信息")
logger.warning("大家好,warning,中等级的警告")
logger.info("大家好,我是INFO,初等级的提示错误")
logger.debug("大家好,我是debug,只是小人物,大家忽略我吧")

执行结果:
在这里插入图片描述

向文件输出日志

向文件输出日志

【步骤】
1、创建logging.getLogger局部日志对象logger,设置其等级
2、创建一个logging.FileHandler对象handler处理器,设置其等级
3、定义日志格式,并赋于handler处理器的格式设置
4、局部日志logger添加处理器handler
5、打印日志

例:

import logging

# 获取logger对象,设置日志级别
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)

# 获取文件处理器,并设置级别
handler = logging.FileHandler("./log.txt")
# handler = logging.FileHandler("./logs/log.csv")
handler.setLevel(logging.INFO)

# 获取并设置文件处理器的日志格式
formatter = logging.Formatter('%(asctime)s,%(name)s,%(levelname)s,%(message)s')
handler.setFormatter(formatter)

# 设置日志处理器
logger.addHandler(handler)

# 打印日志
logger.critical("critical:you are lose")
logger.error("error:you make a big error")
logger.warning("warning:something is warning")
logger.info("INFO:mistake is coming")
logger.debug("debug:just a debug")

执行结果:
在这里插入图片描述

同时向控制台和文件输出日志

【步骤】
1、创建logging.getLogger局部日志对象logger,设置其等级
2、创建一个logging.FileHandler对象handler处理器,设置其等级
3、定义日志格式,并赋于handler处理器的格式设置
4、创建一个logging.StreamHandler对象console流处理器,设置其等级
5、用刚才定义的日志格式,并赋于console流处理器的格式设置
6、局部日志logger添加处理器handler和流处理器console
7、打印日志

例:

import logging

# 创建logging.getLogger局部日志对象logger,设置其等级
logger = logging.getLogger("mylog")
logger.setLevel(level=logging.INFO)

# 创建一个logging.FileHandler对象handler处理器,设置其等级
handler = logging.FileHandler("./log2.txt")
handler.setLevel(logging.INFO)

# 定义日志格式,并赋于handler处理器的格式设置
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 创建一个logging.StreamHandler对象console流处理器,设置其等级
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 局部日志logger添加处理器handler和流处理器console
logger.addHandler(handler)
logger.addHandler(console)

#用刚才定义的日志格式,并赋于console流处理器的格式设置(略)
pass

# 打印日志
logger.critical("critical:you are lose")
logger.error("error:you make a big error")
logger.warning("warning:something is warning")
logger.info("INFO:mistake is coming")
logger.debug("debug:just a debug")

执行结果:
在这里插入图片描述

文件日志的滚动更新

为保证存储空间问题,在实际开发中,通常会将不同业务的日志进行分类存储(即多个不同的logger),并分别设置存储上限;动态地剔除时间久远的日志,以保持有限的存储空间,存储的都是最近期的日志;
logging模块提供了RotatingFileHandler类帮我们实现上述功能;

例:以上一个内容为例,改变handler对象

import logging
from logging.handlers import RotatingFileHandler

# 创建logging.getLogger局部日志对象logger,设置其等级
logger = logging.getLogger("mylog")
logger.setLevel(level=logging.INFO)

# 这里本来需要创建一个logging.FileHandler对象handler处理器,设置其等级
# handler = logging.FileHandler("./log2.txt")

# 更新为创建一个 RotatingFileHandler对象handler处理器,大小为1k,3个副本,并设置其等级
handler = RotatingFileHandler("./log3.txt", maxBytes=1 * 1024, backupCount=3)
handler.setLevel(logging.INFO)

# 定义日志格式,并赋于handler处理器的格式设置
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 创建一个logging.StreamHandler对象console流处理器,设置其等级
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 定义日志格式,并赋于console处理器的格式设置
pass

# 局部日志logger添加处理器handler和流处理器console
logger.addHandler(handler)
logger.addHandler(console)

# 打印日志(1000条)
for i in range(1000):
	logger.critical(str(i)+"critical:you are lose")
	logger.error(str(i)+"error:you make a big error")
	logger.warning(str(i)+"warning:something is warning")
	logger.info(str(i)+"INFO:mistake is coming")
	logger.debug(str(i)+"debug:just a debug")

执行结果:
在这里插入图片描述
在这里插入图片描述

使用日志追踪异常信息

日志最大的功能便是回顾过去,记录异常等相关信息。当我们try到系统发生异常时,我们可以通过上面的方法,用日志文件来自动进行记录,注意把参数exc_info=True填入即可

例:打开一个不存在的文件

import logging
from logging.handlers import RotatingFileHandler

# 创建logging.getLogger局部日志对象logger,设置其等级
logger = logging.getLogger("mylog")
logger.setLevel(level=logging.INFO)

# 创建一个 RotatingFileHandler对象handler处理器,大小1K,1个副本
handler = RotatingFileHandler("./log.txt", maxBytes=1 * 1024, backupCount=1)
handler.setLevel(logging.INFO)

# 定义日志格式,并赋于handler处理器的格式设置
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)


# 创建一个logging.StreamHandler对象console流处理器,设置其等级
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 定义日志格式,并赋于console处理器的格式设置
pass

# 局部日志logger添加处理器handler和流处理器console
logger.addHandler(handler)
logger.addHandler(console)

# 打印日志
logger.critical("critical:you are lose")
logger.error("error:you make a big error")
logger.warning("warning:something is warning")
logger.info("INFO:mistake is coming")
logger.debug("debug:just a debug")

try:
    # 这里打开一个并不存在的文件
    open("nothere.txt", "rb")
except Exception:
    # exc_info=True 一并日志记录系统抛出的异常信息,False则不记录
    logger.error("Faild to open nothere.txt,Exception as following:", exc_info=True)
logger.info("Finish")

执行结果:
在这里插入图片描述

日志配置的继承

在开发中,不同业务模块的日志往往有不同loger和它的配置,但大量重复相同的部分,我们可以通过继承的方式来节省代码;
继承的方式很简单,即在指定logger命名时采用**【父日志名.xxx】**的形式,即可继承父日志对象的配置,如:
父日志:main ——名为main的loger
子日志:main.xxx
孙日志:main.xxx.class
注意这里的继承不要与面向对象中的继承混为一谈;这里的继承仅仅只是把配置信息拿来使用而已

例如:这是一个继承了mylog对象的模块testSon.py 模块内容

import logging

# 定义一个日志对象logger,什么格式也不设,也没有处理器,更没有级别
# 但是命名上为mylog.son
logger = logging.getLogger("mylog.son")

def getSomeLog():
    logger.critical("我是son打印的critical信息")
    logger.error("我是son打印的error信息")
    logger.warning("我是son打印的warning信息")
    logger.info("我是son打印的INFO信息")
    logger.debug("我是son打印的debug信息")

class grandson:

    def __init__(self):
        # 定义一个logger,命名为mylog的孙子对象,同样什么都不设置
        self.logger = logging.getLogger("mylog.son.class")

    # 孙子对象的打印方法
    def grandsonGetLogs(self):
        logger.critical("我是grandson打印的critical信息")
        logger.error("我是grandson打印的error信息")
        logger.warning("我是grandson打印的warning信息")
        logger.info("我是grandson打印的INFO信息")
        logger.debug("我是grandson打印的debug信息")

这是被继承的test.py模块

import logging
from logging.handlers import RotatingFileHandler
import testSon


# 创建logging.getLogger局部日志对象logger,设置其等级
logger = logging.getLogger("mylog")
logger.setLevel(level=logging.INFO)

# 升创建一个 RotatingFileHandler对象handler处理器,大小1K,1个副本,并设置处理器的等级
handler = RotatingFileHandler("./log.txt", maxBytes=1 * 1024, backupCount=1,encoding='utf-8')
handler.setLevel(logging.ERROR)

# 定义日志格式,并赋于handler处理器的格式设置
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 局部日志logger添加处理器handler
logger.addHandler(handler)

# 当前模块打印日志
logger.critical("我是mylog打印的critical信息")
logger.error("我是mylog打印的error信息")
logger.warning("我是mylog打印的warning信息")
logger.info("我是mylog打印的INFO信息")
logger.debug("我是mylog打印的debug信息")

# 执行儿子模块testSon的打印日志
testSon.getSomeLog()
# 执行孙子模块grandson的打印日志
testSon.grandson().grandsonGetLogs()

执行结果:
在这里插入图片描述
在这里插入图片描述

使用JSON文件进行日志配置

日志中的loger固然可以通过继承的方式,简省了很多代码,但是在Python代码中硬编码logging的配置是很不灵活,也不便于管理的;因为不同的loger通常都有不同的处理器,不同的格式,不同的输出方法。而且到处都有。
logging配置的最好的方法是使用一个配置文件来管理;作一个全局的配置。只要管理这个文件就可以了
Python 2.7及以后可以从字典中加载logging配置,也就意味着可以通过读取JSON文件来加载日志的配置;

JSON文件配置模板范本

下面是一个JSON文件存储日志配置的范本,我们可以以此为蓝本,来修改不同logger对象的日志格式、文件存储位置、备份文件和存储上限、输出处理器等信息:
(PS:这里注意使用时要删除中文注释,否则就不是一个标准的JSON文件)

例:

{
  "version": 1,
  "disable_existing_loggers": false,

  // 日志格式
  "formatters": {
    "simple": {
      "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    }
  },
  
  // 处理器格式
  "handlers": {

    // 定义控制台日志的级别和样式
    "console": {
      "class": "logging.StreamHandler",
      "level": "DEBUG",
      "formatter": "simple",
      "stream": "ext://sys.stdout"
    },

    // 定义INFO(以上)级别的日志处理器
    "info_file_handler": {
      "class": "logging.handlers.RotatingFileHandler",
      "level": "INFO",
      "formatter": "simple",
      "filename": "./logs/info.log",
      "maxBytes": 10485760,
      "backupCount": 20,
      "encoding": "utf8"
    },

    // 定义ERROR以上)级别的日志处理器
    "error_file_handler": {
      "class": "logging.handlers.RotatingFileHandler",
      "level": "ERROR",
      "formatter": "simple",
      "filename": "./logs/errors.log",
      "maxBytes": 10485760,
      "backupCount": 20,
      "encoding": "utf8"
    }
  },

  // 定义不同name的logger的日志配置
  "loggers": {
    "mymodule": {
      "level": "ERROR",
      "handlers": [
        "info_file_handler"
      ],
      "propagate": "no"
    }
  },

  // 定义全局日志配置
  "root": {
    "level": "INFO",
    "handlers": [
      "console",
      "info_file_handler",
      "error_file_handler"
    ]
  }
}
加载日志的Json配置,并使用

以上只是Json的范本,使用前需结合实际,修改配置的内容,特别是路径和级别,还要注意根日志的配置。为了使用方便,还可以选择配置好环境变量

例:

import json
import logging
import os
from logging import config

# 加载全局logging配置
# default_path 默认的JSON配置文件路径
# default_level 默认的日志级别
# env_key 通过读取系统环境变量来存储JSON配置文件的路径,名称是自定义的,前提是我们已经手动配置过这个环境变量


# 尝试从环境变量读取日志配置,默认返回None
OSpath = os.getenv("env_key", None)

if OSpath:
    path = OSpath
    print("从环境变量中获取到配置地址:",OSpath)
else:
    path = './config/logConfig.json'
    print("没有从环境变量中获取到配置地址,直接使用了相对地址")

# 从json文件中加载日志配置
if path:
    with open(path,"r",encoding='utf-8') as f:
        # 读取json文件为字典
        mydictconfig = json.load(f)
        print(type(mydictconfig))   #<class 'dict'>

    # 使用字典进行全局日志配置(直接logging.config会出现异常)
    # logging.config.dictConfig(mydictconfig)
    config.dictConfig(mydictconfig)
else:
    # 没有读到配置文件时就配置一个基本的
    logging.basicConfig(level=logging.INFO)

# 使用读取出来的mymodule的配置
logger = logging.getLogger("mymodule")

# 当前模块打印日志
logger.critical("我是mymodule打印的critical信息")
logger.error("我是mymodule打印的error信息")
logger.warning("我是mymodule打印的warning信息")
logger.info("我是mymodule打印的INFO信息")
logger.debug("我是mymodule打印的debug信息")

执行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值