笔记更新于2019年12月4日,
摘要:各种调试方法介绍assert、logging、调试器pdb;单元测试unittest的编写方法、如何运行单元测试;文档测试doctest的编写
写在前面:为了更好的学习python,博主记录下自己的学习路程。本学习笔记基于廖雪峰的Python教程,如有侵权,请告知删除。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ
目录
调试与测试
调试
• print
• 断言 assert
• logging
• 调试器 pdb
• pdb.set_trace( )
• IDE
单元测试 unittest
• 单元测试编写
• 单元测试方法
• 运行单元测试
• setUp( )和tearDown( )
文档测试 doctest
调试与测试
调试
程序编写的过程中会出现各种意想不到的bug,想要一次性写好并成功运行几乎不可能。我们需要知道在运行过程中哪些变量可能会出错,在编写过程中要有一套调试程序的手段来修复bug。下面介绍在Python中常见的调试手段。
最简单粗暴的方法是print,只要在把可能会出错的变量打印出来即可。
def fn(s):
n = s
print('>>> n = %d' % n)
return 10 / n
def main():
fn(0)
main()
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
这样我们就能知道是n = 0导致的错误。这种方法有个很大的问题就是,在程序编写完之后会留下大量的垃圾信息,不好处理。
• 断言 assert
在上面凡是用到print的地方都可以用assert来代替。
assert后面加一个判断语句,该判断为正确时程序正常运行,反之出现错误,打印紧跟的字符串。
def fn(s):
n = s
assert n != 0, '>>> n = 0'
return 10 / n
def main():
fn(0)
main()
运行结果:
Traceback (most recent call last):
...
AssertionError: >>> n = 0
assert 即声明、断言n应该不等于0,但结果n等于0,则断言失败,抛出AssertionError错误,并打印出紧跟的字符串'>>> n = 0'。
相比print,assert可以通过-O参数来关闭,关闭之后所有的assert语句相当于pass。如把上面代码保存为err.py文件,在python解释器中运行:
python -O err.py
运行结果:
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
可见assert被关闭了,解释器打印出了ZeroDivisionError而不是AssertionError。
• logging
logging也是可以把错误输出。
import logging
s = 0
n = s
logging.info('n = %d' % n)
print(10 / n)
运行之后发现出了ZeroDivisionError之外没有其他信息。这是因为logging的信息输出是有级别限制的,我们需要设置级别。
在import logging之后加上:
logging.basicConfig(level=logging.INFO)
这时候再运行:
INFO:root:n = 0
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
显示的错误信息就有logging.info之后的信息了。
logging的好处是允许你指定记录信息的级别。一共有五个级别,从小到大依次是:debug,info,warning,error,critical。默认的level是warning,只有高于等于warning级别的信息才会被打印,当然可以修改level,这就是为什么一开始没有打印信息。
logging的另一个好处是可以通过简单的配置,把信息输出到文件中(后面会介绍如何配置)。
• 调试器 pdb
pdb,即python debugger,Python调试器。调试器可以让程序逐步运行,并查看运行状态。
先写一个简单的py文件:
# err.py
s = '0'
n = int(s)
print(10 / n)
在命令行模式以参数-m pdb启动err.py:
python -m pdb err.py
启动pdb之后,会自动定位到下一步要执行的代码“s = '0'”,然后在(pdb)之后等待输入。
输入字母l命令可以查看err.py的全部代码:
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
输入字母n可以单步执行代码:
(Pdb) n
> c:\users\administrator\err.py(3)()
-> n = int(s)
(Pdb) n
> c:\users\administrator\err.py(4)()
-> print(10 / n)
输入字母p加变量名可以查看该变量:
(Pdb) p s
'0'
(Pdb) p n
0
输入字母q结束调试:
(Pdb) q
• pdb.set_trace( )
set_trace即放置断点。在可能出错的地方添加语句pdb.set_trace()就可以设置一个断点
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
当代码运行到pdb.set_trace()时,就会暂停并自动进入pdb调试环境。可执行上面介绍的命令进行调试。输入c继续运行。
这种方法比pdb的单步调试效率要高一点。
• IDE
Integrated Development Environment,集成开发环境,即IDE。用于程序开发环境的应用程序。
IDE一般包括代码编辑器、编译器、调试器和图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的开发软件服务套。
目前比较好的python IDE有以下几种:
pycharm
Eclipse + PyDev
Visual Studio + PTVS
单元测试 unittest
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如我们想要测试abs函数,给出下面的测试用例:
输入正数,1、100、0.1,期待输出1、10、0.1;
输入负数,-1、-100、-0.1,期待输出1、10、0.1;
输入0,期待输出0;
输入非数值类型,'a'、[ ]、{ },期待输出TypeError
如果abs函数通过了上面的测试,我们就认为abs函数是能够正常运行的。
在编写程序时,有一个测试用例,让程序的行为始终符合测试用例的逻辑,那么就能极大可能地保证程序的正确性。
下面引用廖雪峰官方网站里面的一个例子,来介绍Python中单元测试的编写。
• 单元测试编写
如我们想编写一个Dict类,用途与内置的dict一样,但可以通过属性来访问,像这样:
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
于是编写一个mydict:
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw) # Dict类的实例属性继承dict
def __getattr__(self, key): # 当访问实例key属性,而没有key属性时,调用该方法
try:
return self[key] # 尝试返回键key对应的值value
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
# 若返回失败,显示Dict没有该属性
def __setattr__(self, key, value): # 设置一个赋值方法setattr
self[key] = value
然后开始编写单元测试模块mydict_test.py。
首先我们需要引入Python自带的unittest模块和编写DIct模块,然后定义一个测试类,这个类继承unittest.TestCase
import unittest
from mydict import Dict
class TestDice(unittest.TestCase):
pass
• 单元测试方法
接下来是写测试方法,对每一类测试都需要编写一个test_xxx()方法,以test开头的方法是测试方法,不以test开头的方法不被认为是测试方法,测试的时候是不会被执行。
在继承的unittest.TestCase中有许多内置的条件判断方法,我们直接调用来进行测试。下面是三个常用的测试方法:
assertEqual( )传入两个参数,一个是需要执行的对象,一个是对象执行后期望返回的结果。
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
assertTrue( ),期望括号里面返回的值为True。
self.assertTrue(abs(-1) == 1)
assertRaises( ):,期望冒号后的语句抛出括号里面的错误。
with self.assertRaises(TypeError):
abs('a')
我们用上面的三种测试方法来编写mydict_test.py (以下代码部分除注释外转自廖雪峰的官方网站)
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self): # 测试Dict的实例
d = Dict(a=1, b='test') # 创建一个Dict的实例
self.assertEqual(d.a, 1) # 测试是否能通过属性来访问值
self.assertEqual(d.b, 'test') # 测试是否能通过属性来访问值
self.assertTrue(isinstance(d, dict)) # 测试实例d是否为dict类
def test_key(self): # 测试key属性
d = Dict() # 创建一个Dict的实例
d['key'] = 'value' # 给d添加键key和值value
self.assertEqual(d.key, 'value') # 测试能否通过属性key来访问值value
def test_attr(self): # 测试setattr方法
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self): # 测试访问不存在的key时,打印KeyError
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self): # 测试通过属性访问不存在的key时,打印AttributeError
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
自此我们完成了单元测试的编写,接下来就可以运行单元测试了。
• 运行单元测试
运行单元测试有两种方法,一种是在单元测试模块最后加上两行代码:
if __name__ == '__main__':
unittest.main()
然后直接运行mydict_test.py即可。
另一种是在命令行模式中通过参数-m unittest运行单元测试:
python -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
推荐后一种方法,因为这样可以批量运行多种单元测试。
通过测试的话就会打印出“OK”。
• setUp( )和tearDown( )
补充两个单元测试方法setUp( )和tearDown( )。这两个方法会分别在每调用一个测试方法的前后分别被执行。如在上面的单元测试中,我们加上这两个方法:
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
输出结果就变成:
python -m unittest mydict_test
.....
setUP...
tearDowm...
setUP...
tearDowm...
setUP...
tearDowm...
setUP...
tearDowm...
setUP...
tearDowm...
.
-------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
可见每个方法都前后执行了一次setUp( )和tearDown( )。
这有什么用呢?比如我们的测试方法是需要连接数据库,每个测试方法都要添进行连接与关闭数据库的操作就很麻烦,这时候可以通过setUp( )来连接数据库和tearDown( )来关闭数据库,从而减少了许多重复的代码。
文档测试 doctest
Python中内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
比如我们写这样一段代码:
def abs(n):
'''
Function to get absolute value of number.
Example:
>>> abs(1)
1
>>> abs(-1)
1
>>> abs(0)
0
>>> abs('a')
Traceback (most recent call last):
...
TypeError: bad operand type for abs(): 'str'
'''
return n if n >= 0 else (-n)
通过单引号'''与‘’’括起的内容是注释,此时我们可以通过doctest模块来进行测试。
doctest会严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。
遇到>>>时即开始执行代码,有指示符>>>为输入,没有指示符>>>的为输出。当测试异常时,可用...来表示其中的输出。
我们来编写test.py并进行文档测试。
# -*- coding: utf-8 -*-
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1) #这里输入fact(1)
1 #期待输出的值为1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last): #当遇到错误时期待输出的错误信息
...
ValueError
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()
需要注意的是,在第一个>>>之前可以进行函数的描述,在>>>之后的内容就会被doctest执行。
最后三行代码表示,只有在命令行直接运行该文件时会进行测试,而被引用时不会进行测试,所以不必担心doctest会在非测试环境下运行。
运行这个文件会发现什么都没有输出,这就说明编写的doctest运行都是正确的。如果测试代码与函数运行结果不一致,则会出错,比如我们将
>>> fact(10)
3628800
改为:
>>> fact(10)
362880
运行结果:
**********************************************************************
File "test.py", line 11, in __main__.fact
Failed example:
fact(10)
Expected:
362880
Got:
3628800
**********************************************************************
1 items had failures:
1 of 3 in __main__.fact
***Test Failed*** 1 failures.
显示fact(10)的期待值为362880,而实际值为3628800。
以上就是本节的全部内容,感谢你的阅读。
下一节内容:20.IO编程
有任何问题与想法,欢迎评论与吐槽。
和博主一起学习Python吧( ̄▽ ̄)~*