【Python入门】19.调试器pdb、单元测试unittest和文档测试doctest

笔记更新于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

最简单粗暴的方法是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)<module>()
-> n = int(s)
(Pdb) n
> c:\users\administrator\err.py(4)<module>()
-> 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吧( ̄▽ ̄)~*

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容