0%

Python-调试日志与测试

Python 在程序测试、调试方面的最佳实践。如:

  • logging:【标准库】功能齐全且灵活的日志记录模块,可方便地设置输出日志的等级、输出方式、保存路径、日志文件回滚。
  • pytest:小型测试,功能测试 、单元测试
  • converage:显示测试覆盖率,将结果输出到控制台或 HTML 页面,指明哪些具体地方未被覆盖到。
  • doctest:对 Python 的 docstring 注释部分的测试用例进行测试
  • unittest:【标准库】单元测试框架,编写用例 、组织用例、执行用例、输出报告等
  • pdb:简单而够用的控制台模式 Python 调试器
  • nose:Python 测试框架
  • pydoc:从 python 代码中获取 docstring,并生成帮助信息

概述

  • bug:程序编写的问题,如要输出整数却输出了字符串,这种错误,必须修复。
    • 调试:跟踪程序的执行,查看变量的值是否正确
    • 测试:在程序修改后反复运行,确保程序输出符合我们编写的测试
  • 用户使用错误:如 email 的格式,须检查用户输入来避免。
  • 无法预测的异常:如磁盘满了,无法写入;网断了,无法连接。为了保证程序的健壮性与容错性,避免程序崩溃,必须处理。

logging

logging:【标准库】功能齐全且灵活的日志记录模块,可方便地设置输出日志的等级、输出方式、保存路径、日志文件回滚。

  • 日志是对软件执行时所发生事件的追踪方式。常用于记录异常的错误信息,并让程序继续执行下去。
  • 在最简单的情况下,日志消息被发送到文件或 sys.stderr
  • 日志设计的好坏决定了问题排查的难易程度,对于日志的评价标准有三个:
    • 覆盖度:是否对程序运行应记录处进行记录,如异常、外部调用、一条调用链路上的入口、出口和路径关键点上等。
    • 清晰度:清晰的表达程序目前的状态,统一风格。
    • 信息量:调用的上下文、外部的返回值,用于查询的关键字等,便于分析信息。
      对于线上系统来说,一般可以通过调整日志级别来控制日志的数量,所以打印日志的代码只要不对阅读造成障碍,基本上都是可以接受的。

结构

  • 日志系统可以直接从 Python 配置,也可以从用户配置文件加载,以便自定义日志记录而无需更改应用程序。
  • Logger: 日志器,提供程序可用接口
  • Handler: 处理器,将 logger 创建的日志发送到合适目的地输出
    • StreamHandler:将日志信息输出到 sys.stdout, sys.stderr 或类文件对象
    • FileHander:将日专信息输出到磁盘文件
    • NullHandler:空操作 handler
  • Filter:过滤器,提供更细的控制,控制输出哪条,丢弃哪条
  • Formatter:格式化器,决定日志输出格式
  • Logger 通过 handler 将日志输出到不同目标位置,可以设置多个处理器输出到不同位置
    每个 handler 可以设置自己的 filter,实现日志过滤
    每个 handler 可以设置自己的 formater, 实现同一条日志 以不同格式输出到不同地方

使用

简单模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import logging
# filename 指定日志存放文件,level 指定 logging 级别
logging.basicConfig(filename="out.log", # 指定日志文件名
level=logging.INFO,
filemode='a', # 指定日志文件的打开模式
format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s)
logging.debug('test')

datefmt: 指定时间格式,同 time.strftime(),默认为%Y-%m-%d %H:%M:%S
level: 设置日志级别,默认为 logging.WARNING
stream: 指定将日志的输出流,可以指定输出到 sys.stderr,sys.stdout 或者文件,默认输出到 sys.stderr,当 stream 和 filename 同时指定时,stream 被忽略
format 参数指定输出的格式和内容:
%(name)s Logger 的名字
%(levelno)s 数字形式的日志级别
%(levelname)s 文本形式的日志级别
%(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
%(filename)s 调用日志输出函数的模块的文件名
%(module)s 调用日志输出函数的模块名
%(funcName)s 调用日志输出函数的函数名
%(lineno)d 调用日志输出函数的语句所在的代码行
%(created)f 当前时间,用 UNIX 标准的表示时间的浮 点数表
%(relativeCreated)d 输出日志信息时的,自 Logger 创建以 来的毫秒数
%(asctime)s 字符串形式的当前时间。默认格式是 "年-月-日 时:分:秒,毫秒",'%a, %d %b %Y %H:%M:%S',
%(thread)d 线程 ID。可能没有
%(threadName)s 线程名。可能没有
%(process)d 进程 ID。可能没有
%(message)s 用户输出的消息
1
2
3
4
5
6
TimedRotatingFileHandler(
filename, # 输出日志文件名的前缀
[when, # 字符串,“S”: Seconds, “M”: Minutes, “H”: Hours, “D”: Days, “W”: Week day (0=Monday), “midnight”: Roll over at midnight
[interval, # 指等待多少个单位 when 的时间后,Logger 会自动重建文件,当然,这个文件的创建取决于 filename+suffix,若这个文件跟之前的文件有重名,则会自动覆盖掉以前的文件,所以有些情况 suffix 要定义的不能因为 when 而重复。
[backupCount # 保留日志个数。默认的 0 是不会自动删除掉日志。若设 10,则在文件的创建过程中库会判断是否有超过这个 10,若超过,则会从最先创建的开始删除。
]]])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def setup_logger(log_file, name='test', log_level=logging.DEBUG):
# 创建 logger
my_logger = logging.getLogger(name)
my_logger.setLevel(log_level)
# 创建 handler
handler_file_size = logging.handlers.RotatingFileHandler(
filename=log_file, mode='a', maxBytes=1 * 1024 * 1024, backupCount=3, encoding='utf-8')
# 设置输出格式
formatter = logging.Formatter(
# 时间 文件名 文件行号 函数名 日志等级 输出的信息
fmt="[%(asctime)s %(filename)s:%(lineno)d:%(funcName)s]:%(levelname)s: %(message)s",
datefmt='%m-%d %H:%M:%S')
handler_file_size.setFormatter(formatter)
my_logger.addHandler(handler_file_size)
# 设置日志器输出到 print 流
print_stream = logging.StreamHandler()
print_stream.setFormatter(formatter)
my_logger.addHandler(print_stream)
return my_logger

logger 日志器

  • 可代码配置,也可使用 conf 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
logger=logging.getLogger('mylogger') # 日志器,提供程序可用接口
logger.setlevel(logging.DEBUG)
# 如果把 logger 的级别设置为 INFO, 那么小于 INFO 级别的日志都不输出(即 debug 的不输出),大于等于 INFO 级别的日志都输出,
logger.debug('test message') # 等级 1,调试,最低,如记录算法中每个循环的中间状态。
logger.info("test message") # 等级 2,信息,处理请求或状态变化等日常事务,确认程序按预期运行
logger.warning("test message") # 等级 3,警告,发生重要的事,但并不是错误,如用户登录密码错误,程序仍按预期进行
logger.error("foobar") # 等级 4,错误,程序的某些功能已经不能正常执行。如 IO 操作失败,或连接失败
logger.critical("foobar") # 等级 5,危急,表明程序已不能继续执行,如内存、磁盘耗尽,一般较少使用

try:
open("sklearn.txt","rb")
except (SystemExit,KeyboardInterrupt):
raise
except Exception:
logger.error("Faild to open sklearn.txt from logger.error",exc_info = True)
logger.info("Finish")

文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

这是 logging.conf 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

pytest

#最佳实践

pytest:小型测试,功能测试 、单元测试

【选型】

  • unittest:标准库,但代码相比于 pytest 更冗长、不如 pytest 灵活和简介
    • 项目需求:如果你在一个已有的基于 unittest 的项目中工作,或者你的团队已经习惯了 unittest 的结构,继续使用 unittest 会更方便一些。
    • 开发效率:如果你是一个新项目或者你重视开发效率,简洁性,和快速反馈,pytest 是一个更好的选择。
    • 集成与支持:如果你的 CI/CD 流程、代码覆盖率工具等已经很好地与 unittest 集成,使用 unittest 会减少额外的配置和兼容性问题。
  • 如果你重视 简洁性 和 易用性,并且希望更高效地编写测试代码,建议使用 pytest。
  • 如果你偏向于使用标准库,并且更看重 稳定性 和 企业级支持,那么 unittest 可能是更好的选择。
  • unittest 能做的 pytest 都能做,pytext 比 unittest 理边界,支持用例出错重跑、xdist 插件等
    • 【】
  • 特点:
    • 简单灵活,容易上手
    • 支持参数化
    • 能够支持简单的单元测试和复杂的功能测试,还可以用来做 selenium/appnium 等自动化测试、接口自动化测试(pytest+requests)
1
pip install -U pytest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 使用Pytest进行自动化测试
import pytest

# 待测试的函数
def add_numbers(x, y):
return x + y

# 函数的测试用例
def test_addition():
assert add_numbers(1, 2) == 3
assert add_numbers(-1, 1) == 0
assert add_numbers(0, 0) == 0
assert add_numbers(10, 5) == 15

if __name__ == "__main__":
pytest.main()

基本使用

编写规则

  • 测试文件以 test_开头(以_test 结尾也可以)
  • 测试类以 Test 开头,并且不能带有 init 方法
  • 测试函数以 test_ 开头
  • 断言使用基本的 assert 即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 运行后生成测试报告(pytest-html)
pytest --html=report.html

# 运行指定的case
# 定义class时,需要以T开头,不然pytest是不会去运行该class的
pytest test_se.py
pytest test_se.py::TestClassOne
pytest test_se.py::TestClassTwo::test_one

# 多进程运行case(pytest-xdist)
pytest test_se.py -n NUM

# 重试运行cases(pytest-rerunfailures)
pytest test_se.py --reruns NUM

# 显示print内容(-s)
pytest test_se.py -s -n 4

converage

converage:显示测试覆盖率,将结果输出到控制台或 HTML 页面,指明哪些具体地方未被覆盖到。

1
converage [file] [args]

doctest

doctest:对 Python 的 docstring 注释部分的测试用例进行测试

1
2
3
4
5
6
7
8
9
10
def average(values):
"""Computes the arithmetic mean of a list of numbers.

>>> print(average([20, 30, 70]))
40.0
"""
return sum(values) / len(values)

import doctest
doctest.testmod() # automatically validate the embedded tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import doctest
def fun1(x):
"""这是文档测试的内容
>>> ceshi(2)
1
>>> ceshi(3)
0
"""#End
if x%2==0:
return 1
else:
return 0
if __name__ == "__main__":
doctest.testmod(verbose=True) # doctest.testmod 是测试模块,verbose 默认是 False,意思是出错才用提示;True,对错都有执行结果

unittest

unittest:【标准库】单元测试框架,编写用例 、组织用例、执行用例、输出报告等

  • 允许在一个单独的文件中文虎更全面的测试集
1
2
3
4
5
6
7
8
9
10
11
12
13
import unittest

class TestStatisticalFunctions(unittest.TestCase):

def test_average(self):
self.assertEqual(average([20, 30, 70]), 40.0)
self.assertEqual(round(average([1, 5, 7]), 1), 4.3)
with self.assertRaises(ZeroDivisionError):
average([])
with self.assertRaises(TypeError):
average(20, 30, 70)

unittest.main() # Calling from the command line invokes all tests
  • TesteCase:测试用例
  • TestSuite:测试用例集合
  • TestLoader:加载 TestCase 到 TestSuite
  • TestRunner:执行测试用例,并将结果保存晉 TestRunner
  • TestFixture
  • 相比于 doctest,unittest 可以与待测试的函数分开写

过程

1
2
3
4
5
6
7
8
9
10
11
12
# 待测试模块 myfunc
def is_prime(number):
if number<0 or number in (0,1):
return False
for element in range(2,number):
if number % element ==0:
return False
return True
def add(a,b):
return a+b
def divide(a,b):
return a/b
1
2
3
4
import unittest
from myfunc import is_prime,add,divide
class TestMyFunc(unittest.TestCase):
'''测试 myfunc'''

pdb

pdb:简单而够用的控制台模式 Python 调试器

Python 的 pdb 可以让我们以单步方式执行代码。

1
2
python -m pdb test.py
# 以参数`-m pdb`启动后,pdb 定位到下一步要执行的代码`-> s = '0'`。输入命令`l`来查看代码:

nose

nose:Python 测试框架

pydoc

pydoc:从 python 代码中获取 docstring,并生成帮助信息

1
2
3
# test.py
python -m pydoc test # test 为模块名
python -m pydoc -w 模块名 # 生成 HTML 帮助文档

traceback

traceback:【标准库】用于提取,格式化和打印 Python 程序的堆栈跟踪。

  • 常用于跟踪异常返回信息
  • 常用:traceback.format_exc()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import sys
import traceback
def func_1():
a = 1
b = 0
c = a / b
return c
try:
func_1()
except Exception as e:
print(f'box_process: {e.__traceback__.tb_frame.f_globals["__file__"]}, {e.__traceback__.tb_lineno}, {e},')
print('str(Exception):\t', str(e))
print('str(e):\t\t', str(e))
print('repr(e):\t', repr(e))

# Get information about the exception that is currently being handled
# 常用
print('traceback.format_exc():', traceback.format_exc())



import traceback
traceback.print_stack(file=sys.stdout)


try:
if str(123) > 5:
print('这是一个无法执行的错误')
except Exception:
traceback.print_exc() # 返回错误信息
# 写入文件
traceback.print_exc(file=open('log.log', mode='a', encoding='utf-8'))

# 控制台输出结果:
# Traceback (most recent call last):
# File "D:/pycharm/S9/pythontext/main.py", line 27, in <module>
# if str(123) > 5:
# TypeError: '>' not supported between instances of 'str' and 'int'

基本使用

1
2
3
4
5
6
7
8
9
10
11
trackback.format_exc() # 返回字符串
trackback.print_exc() # 打印,可接受file参数直接写入到文件


def func(num1, num2):
try:
x = num1 * num2
y = num1 / num2
return x, y
except:
print(f'--- {traceback.format_exc()} ---')
给博主来一杯卡布奇诺