Python(十四)测试调试和异常
Posted HT . WANG
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python(十四)测试调试和异常相关的知识,希望对你有一定的参考价值。
1.测试stdout输出
写个测试来证明标准输出,会将文本打印到屏幕上面
使用unitest框架进行测试
# mymodule.py
def urlprint(protocol, host, domain):
url = '://.'.format(protocol, host, domain)
print(url)
from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import mymodule
class TestURLPrint(TestCase): #单元测试类
def test_url_gets_to_stdout(self):
protocol = 'http'
host = 'www'
domain = 'example.com'
expected_url = '://.\\n'.format(protocol, host, domain) #给定标准输出
with patch('sys.stdout', new=StringIO()) as fake_out: #patch模拟一种输出方式
mymodule.urlprint(protocol, host, domain)
self.assertEqual(fake_out.getvalue(), expected_url) #通过断言判断二者是否一致
2.在单元测试中给对象打补丁
写的单元测试中需要给指定的对象打补丁, 用来断言它们在测试中的期望行为(比如,断言被调用时的参数个数,访问指定的属性等)
(1)将它当做装饰器使用
from unittest.mock import patch
import example
@patch('example.func')
def test1(x, mock_func):
example.func(x) # Uses patched example.func
mock_func.assert_called_with(x) #断言被调用时的参数
(2)被当做一个上下文管理器
with patch('example.func') as mock_func:
example.func(x) # Uses patched example.func
mock_func.assert_called_with(x) #断言被调用时的参数
(3)可以手动的使用它打补丁
p = patch('example.func')
mock_func = p.start() #限制作用域
example.func(x)
mock_func.assert_called_with(x) #断言被调用时的参数
p.stop() #限制作用域
注意:patch()
接受一个已存在对象的全路径名,将其替换为一个新的值。 原来的值会在装饰器函数或上下文管理器完成后自动恢复回来。因此patch()起到
限制测试作用域的作用
3.在单元测试中测试异常情况
写个测试用例来准确的判断某个异常是否被抛出。
import unittest
# A simple function to illustrate
def parse_int(s):
return int(s)
class TestConversion(unittest.TestCase):#测试类
def test_bad_int(self): #测试用例
self.assertRaises(ValueError, parse_int, 'N/A') #'N/A'表示错误提示
对比手动异常检测:
class TestConversion(unittest.TestCase):
def test_bad_int(self):
try:
r = parse_int('N/A')
except ValueError as e:
self.assertEqual(type(e), ValueError)
else:
self.fail('ValueError not raised') #需要注意 没有任何异常抛出情况下需要设置处理措施
4.将测试输出用日志记录到文件中
将单元测试的输出写到到某个文件中去,而不是打印到标准输出
(1)将运行测试的结果打印到标准输出上
import unittest
class MyTest(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()
(2)重定向输出到文件
import sys
def main(out=sys.stderr, verbosity=2):
loader = unittest.TestLoader() #组装测试套件
suite = loader.loadTestsFromModule(sys.modules[__name__]) #从测试类中扫描收集测试用例
unittest.TextTestRunner(out,verbosity=verbosity).run(suite) #测试运行类
if __name__ == '__main__':
with open('testing.out', 'w') as f:
main(f)
5.忽略或期望测试失败
unittest
模块有装饰器可用来控制对指定测试方法的处理
import unittest
import os
import platform
class Tests(unittest.TestCase):
def test_0(self):
self.assertTrue(True)# 输出结果:ok
@unittest.skip('skipped test') #skip() 装饰器能被用来忽略某个你不想运行的测试 输出结果:skipped 'skipped test'
def test_1(self):
self.fail('should have failed!')
@unittest.skipIf(os.name=='posix', 'Not supported on Unix') #指定测试系统为posix 不支持unix系统 输出结果:skipped 'Not supported on Unix'
def test_2(self):
import winreg
@unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific test') #指定测试平台为mac 输出结果:ok(保证使用mac电脑测试)
def test_3(self):
self.assertTrue(True)
@unittest.expectedFailure
def test_4(self):
self.assertEqual(2+2, 5)
if __name__ == '__main__':
unittest.main()
6.捕获异常
(1)处理多个异常
不创建大量重复代码就能处理所有的可能异常
try:
client_obj.get_url(url)
except (URLError, ValueError):
client_obj.remove_url(url)
except SocketTimeout:
client_obj.handle_url_timeout(url)
注意:异常会有层级关系,对于这种情况,你可能使用它们的一个基类来捕获所有的异常
同时except
语句是顺序检查的,第一个匹配的会执行 所以异常被第一个能识别的捕获
(2)捕获全部异常
想要捕获所有的异常,可以直接捕获 Exception
即可
try:
...
except Exception as e:
...
log('Reason:', e) # Important!
注意:这个将会捕获除了 SystemExit
、 KeyboardInterrupt
和 GeneratorExit
之外的所有异常。 如果你还想捕获这三个异常,将 Exception
改成 BaseException
即可。
(3)捕获自定义异常
自定义异常类应该总是继承自内置的 Exception
类, 或者是继承自那些本身就是从 Exception
继承而来的类
class NetworkError(Exception):
pass
class HostnameError(NetworkError):
pass
class TimeoutError(NetworkError):
pass
class ProtocolError(NetworkError):
pass
try:
msg = s.recv()
except TimeoutError as e:
...
except ProtocolError as e:
...
7.输出警告信息
程序能生成警告信息(比如废弃特性或使用问题)
要输出一个警告消息,可使用 warning.warn()
函数。
import warnings
def func(x, y, logfile=None, debug=False):
if logfile is not None:
warnings.warn('logfile argument deprecated', DeprecationWarning)
...
warn()
的参数是一个警告消息和一个警告类
警告类有如下几种:
- UserWarning,
- DeprecationWarning,
- SyntaxWarning,
- RuntimeWarning,
- ResourceWarning,
- FutureWarning.
对警告的处理取决于你如何运行解释器以及一些其他配置
-W
选项能控制警告消息的输出。-
-W all
会输出所有警告消息, -W ignore
忽略掉所有警告,-W error
将警告转换成异常
8.调试程序崩溃错误
例:
# sample.py
def func(n):
return n + 10
func('Hello')
如果你的程序因为某个异常而崩溃,运行 python3 -i someprogram.py
可执行简单的调试。 -i
选项可让程序结束后打开一个交互式shell。
bash % python3 -i sample.py
Traceback (most recent call last):
File "sample.py", line 6, in <module>
func('Hello')
File "sample.py", line 4, in func
return n + 10
TypeError: Can't convert 'int' object to str implicitly
>>> func(10)
20
>>>
部分编译环境下,可以在程序崩溃后打开Python的调试器
>>> import pdb
#启动python自带调试器
>>> pdb.pm()
> sample.py(4)func()
-> return n + 10
(Pdb) w
sample.py(6)<module>()
-> func('Hello')
> sample.py(4)func()
-> return n + 10
(Pdb) print n
'Hello'
(Pdb) q
>>>
完整命令 | 简写命令 | 描述 |
args | a | 打印当前函数的参数 |
break | b | 设置断点 |
clear | cl | 清除断点 |
condition | 无 | 设置条件断点 |
continue | c或者cont | 继续运行,知道遇到断点或者脚本结束 |
disable | 无 | 禁用断点 |
enable | 无 | 启用断点 |
help | h | 查看pdb帮助 |
ignore | 无 | 忽略断点 |
jump | j | 跳转到指定行数运行 |
list | l | 列出脚本清单 |
next | n | 执行下条语句,遇到函数不进入其内部 |
p | p | 打印变量值,也可以用print |
quit | q | 退出 pdb |
return | r | 一直运行到函数返回 |
tbreak | 无 | 设置临时断点,断点只中断一次 |
step | s | 执行下一条语句,遇到函数进入其内部 |
where | w | 查看所在的位置 |
! | 无 | 在pdb中执行语句 |
9.性能测试
程序运行所花费的时间并做性能测试
(1)简单的想测试下你的程序整体花费的时间, 通常使用Unix时间函数time就行
bash % time python3 someprogram.py
real 0m13.937s
user 0m12.162s
sys 0m0.098s
bash %
需要一个程序各个细节的详细报告,可以使用 cProfile
模块
bash % python3 -m cProfile someprogram.py
859647 function calls in 16.016 CPU seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
263169 0.080 0.000 0.080 0.000 someprogram.py:16(frange)
513 0.001 0.000 0.002 0.000 someprogram.py:30(generate_mandel)
262656 0.194 0.000 15.295 0.000 someprogram.py:32(<genexpr>)
1 0.036 0.036 16.077 16.077 someprogram.py:4(<module>)
262144 15.021 0.000 15.021 0.000 someprogram.py:4(in_mandelbrot)
1 0.000 0.000 0.000 0.000 os.py:746(urandom)
1 0.000 0.000 0.000 0.000 png.py:1056(_readable)
1 0.000 0.000 0.000 0.000 png.py:1073(Reader)
1 0.227 0.227 0.438 0.438 png.py:163(<module>)
512 0.010 0.000 0.010 0.000 png.py:200(group)
...
bash %
(2)测试个别函数消耗时间 可以采用装饰器方法
# timethis.py
import time
from functools import wraps
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
r = func(*args, **kwargs)
end = time.perf_counter()
print('. : '.format(func.__module__, func.__name__, end - start))
return r
return wrapper
>>> @timethis #要使用这个装饰器,只需要将其放置在你要进行性能测试的函数定义前即可
... def countdown(n):
... while n > 0:
... n -= 1
...
>>> countdown(10000000)
__main__.countdown : 0.803001880645752
>>>
(3)要测试某个代码块运行时间,可以定义一个上下文管理器
from contextlib import contextmanager
@contextmanager
def timeblock(label):
start = time.perf_counter()
try:
yield
finally:
end = time.perf_counter()
print(' : '.format(label, end - start))
>>> with timeblock('counting'):
... n = 10000000
... while n > 0:
... n -= 1
...
counting : 1.5551159381866455
>>>
注意:
当执行性能测试的时候,需要注意的是你获取的结果都是近似值。 time.perf_counter()
函数会在给定平台上获取最高精度的计时值。 不过,它仍然还是基于时钟时间,很多因素会影响到它的精确度,比如机器负载。
10.优化程序运行效能
(1)使用函数
# somescript.py
import sys
import csv
with open(sys.argv[1]) as f:
for row in csv.reader(f):
# Some kind of processing
pass
#定义在全局范围的代码运行起来要比定义在函数中运行慢的多 因此,如果你想让程序运行更快些,只需要将脚本语句放入函数中即可
# somescript.py
import sys
import csv
def main(filename):
with open(filename) as f:
for row in csv.reader(f):
# Some kind of processing
pass
main(sys.argv[1])
(2)尽可能去掉属性访问
每一次使用点(.)操作符来访问属性的时候会带来额外的开销。 它会触发特定的方法,比如 __getattribute__()
和 __getattr__()
,这些方法会进行字典操作操作。
import math
def compute_roots(nums):
result = []
for n in nums:
result.append(math.sqrt(n))
return result
# Test
nums = range(1000000)
for n in range(100):
r = compute_roots(nums)
from math import sqrt # 使用 from module import name 这样的导入形式,以及使用绑定的方法 用 sqrt() 代替了 math.sqrt()
def compute_roots(nums):
result = []
result_append = result.append
for n in nums:
result_append(sqrt(n))# The result.append() 方法被赋给一个局部变量 result_append ,然后在内部循环中使用它 这种方法只有在大量重复代码中(循环)有意义
return result
(3)使用局部变量
import math
def compute_roots(nums):
sqrt = math.sqrt #sqrt 从 math 模块被拿出并放入了一个局部变量中 加速原因是因为对于局部变量 sqrt 的查找要快于全局变量 sqrt
result = []
result_append = result.append
for n in nums:
result_append(sqrt(n))
return result
(4)避免不必要的抽象
任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装你的代码时,都会让程序运行变慢。
(5)使用内置的容器
内置的数据类型比如字符串、元组、列表、集合和字典都是使用C来实现的,运行起来非常快。 如果你想自己实现新的数据结构(比如链接列表、平衡树等), 那么要想在性能上达到内置的速度几乎不可能,
开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系以上是关于Python(十四)测试调试和异常的主要内容,如果未能解决你的问题,请参考以下文章
RK3399驱动开发 | 10 - RK3399以太网gmac调试