文章目录
第一节:断言与单元测试
断言
格式:
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信息")
执行结果: