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 logginglogging.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, [interval, [backupCount ]]])
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 ): my_logger = logging.getLogger(name) my_logger.setLevel(log_level) 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_stream = logging.StreamHandler() print_stream.setFormatter(formatter) my_logger.addHandler(print_stream) return my_logger
logger 日志器 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.debug('test message' ) logger.info("test message" ) logger.warning("test message" ) logger.error("foobar" ) logger.critical("foobar" ) 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 loggingimport logging.configlogging.config.fileConfig('logging.conf' ) logger = logging.getLogger('simpleExample' ) 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 =StreamHandlerlevel=DEBUG formatter=simpleFormatter args=(sys.stdout,) [formatter_simpleFormatter] format =%(asctime)s - %(name)s - %(levelname)s - %(message)sdatefmt=
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pytestdef 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 页面,指明哪些具体地方未被覆盖到。
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 doctestdoctest.testmod()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import doctestdef fun1 (x ): """这是文档测试的内容 >>> ceshi(2) 1 >>> ceshi(3) 0 """ if x%2 ==0 : return 1 else : return 0 if __name__ == "__main__" : doctest.testmod(verbose=True )
unittest unittest:【标准库】单元测试框架,编写用例 、组织用例、执行用例、输出报告等
1 2 3 4 5 6 7 8 9 10 11 12 13 import unittestclass 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()
TesteCase:测试用例 TestSuite:测试用例集合 TestLoader:加载 TestCase 到 TestSuite TestRunner:执行测试用例,并将结果保存晉 TestRunner TestFixture 相比于 doctest,unittest 可以与待测试的函数分开写 过程 1 2 3 4 5 6 7 8 9 10 11 12 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 unittestfrom myfunc import is_prime,add,divideclass TestMyFunc (unittest.TestCase): '''测试 myfunc'''
pdb pdb:简单而够用的控制台模式 Python 调试器
Python 的 pdb 可以让我们以单步方式执行代码。
nose nose:Python 测试框架
pydoc pydoc:从 python 代码中获取 docstring,并生成帮助信息
1 2 3 python -m pydoc test python -m pydoc -w 模块名
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 sysimport tracebackdef 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)) print ('traceback.format_exc():' , traceback.format_exc()) import tracebacktraceback.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' ))
基本使用 1 2 3 4 5 6 7 8 9 10 11 trackback.format_exc() trackback.print_exc() def func (num1, num2 ): try : x = num1 * num2 y = num1 / num2 return x, y except : print (f'--- {traceback.format_exc()} ---' )