Python(10)错误、调试、测试(下)

简介: Python(10)错误、调试、测试(下)

三、调试


  • 在实际工作中,一次性能写完代码并且可以正常运行的概率很小,因为总会有各种各样的bug需要处理
  • 有的bug很简单,看看错误输出就可以解决,有的bug很复杂,需要知道出错时,哪些变量是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug
  • 下面来看几种调试的方法:


- print


  • 第一种方法简单粗暴,就是把print()把可能有问题的变量打印出来看看,例如:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def foo(s):
    num = int(s)
    print('>>>num = %s' % num)
    return print(10 / num)
def main():
    foo('0')
main()
#输出
>>>num = 0    #输出了num变量的值
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 11, in <module>
    main()
  File "f:\MY_python\Python\test2.py", line 9, in main
    foo('0')
  File "f:\MY_python\Python\test2.py", line 6, in foo
    return print(10 / num)
ZeroDivisionError: division by zero
- 但是使用'print()'最大的坏处就是在调试完代码之后,需要把'print()'语句注释或者删除掉,一想到程序里各种'print()',运行结果里面包含很多打印的无关信息,这个时候我们可以使用第二种方法

- 断言assert


  • 只要是使用了print()语句来辅助查看的地方,都可以使用断言(assert)来代替,例如:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def foo(s):
    num = int(s)
    assert num != 0,'num is 0 !!'
    return print(10 / num)
def main():
    foo('0')
main()
#输出
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 11, in <module>
    main()
  File "f:\MY_python\Python\test2.py", line 9, in main
    foo('0')
  File "f:\MY_python\Python\test2.py", line 5, in foo
    assert num != 0,'num is 0 !!'
AssertionError: num is 0 !!   #最后输出的是AssertionError错误类型
- 以assert num != 0,'num is 0 !!'为例,来看一下assert的使用方法: assert num != 0 ,当表达式num != 0 为'False'时,就会输出后面的语句'num is 0 !!',而assert本身输出的错误类型是AssertionError,

但是如果只是单纯的把print()换成了assert(),其实也好不到哪去,但是在启动Python解释器可以使用-O参数来关闭assert,例如:

- 加-O来执行py文件
PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe -O f:/MY_python/Python/test2.py
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 11, in <module>
    main()
  File "f:\MY_python\Python\test2.py", line 9, in main
    foo('0')
  File "f:\MY_python\Python\test2.py", line 6, in foo
    return print(10 / num)
ZeroDivisionError: division by zero  #可以看到错误类型不再是AssertionError
  • 关闭assert后,可以把所有的assert语句当作pass看待


- logging


  • print()替换为logging是第三种方式,和assert相比,logging不会抛出错误,但是可以输出到文件,例如:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10/n)
#输出
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 8, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 使用'logging.info()'就可以输出一段文本,但是从上面的代码可以看出来,使用了'logging.info()'之后并没有输出相应的文本,这是因为logging有等级之分,这个时候需要再加一段代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10/n)
#输出
INFO:root:n = 0
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 9, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 成功输出了相应文本

使用logging时,允许指定记录信息的级别,总共有debug、info、warning、error、critical等五个级别,其中默认级别是warning,上面使用了INFO级别


日志等级说明:

'DEBUG':程序调试bug时使用
'INFO':程序正常运行时使用
'WARNING':程序未按预期运行时使用,但并不是错误,例如:用户登录时密码错误
'ERROR':程序出错误时使用,例如:IO操作失败
'CRITICAL':特别严重的问题,导致程序不能再继续运行时使用,例如:磁盘空间为空,一般很少使用

注意:


在logging中,根据等级从低到高的顺序是:

DEBUG < INFO < WARNING < ERROR < CRITICAL

在定义logging的等级时,输出等级如果大于等于定义的等级,最终还是会继续输出,例如:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
logging.debug('n = %d' % n)
logging.warning('n = %d' % n)
logging.error('n = %d' % n)
logging.critical('n = %d' % n)
print(10/n)
#输出
INFO:root:n = 0
WARNING:root:n = 0
ERROR:root:n = 0
CRITICAL:root:n = 0
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 13, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 可以看到定义的等级是'INFO',其中'DEBUG'等级比'INFO'低,所以'logging.debug('n = %d' % n)'语句没有输出,修改一下代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logging.basicConfig(level=logging.DEBUG)
s = '0'
n = int(s)
logging.info('n = %d' % n)
logging.debug('n = %d' % n)
logging.warning('n = %d' % n)
logging.error('n = %d' % n)
logging.critical('n = %d' % n)
print(10/n)
#输出
INFO:root:n = 0
DEBUG:root:n = 0
WARNING:root:n = 0
ERROR:root:n = 0
CRITICAL:root:n = 0
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 13, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 可以看到,因为'DEBUG'等级就是最低的了,所以其他等级的输出都可以正常输出,再次修改代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
logging.basicConfig(level=logging.CRITICAL)
s = '0'
n = int(s)
logging.info('n = %d' % n)
logging.debug('n = %d' % n)
logging.warning('n = %d' % n)
logging.error('n = %d' % n)
logging.critical('n = %d' % n)
print(10/n)
#输出:
CRITICAL:root:n = 0
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 13, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 这里因为'CRITICAL'等级就是最高的了,所以其他等级的输出信息都无法输出了,现在来看如果不指定等级
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
#logging.basicConfig(level=logging.CRITICAL)
s = '0'
n = int(s)
logging.info('n = %d' % n)
logging.debug('n = %d' % n)
logging.warning('n = %d' % n)
logging.error('n = %d' % n)
logging.critical('n = %d' % n)
print(10/n)
#输出
WARNING:root:n = 0
ERROR:root:n = 0
CRITICAL:root:n = 0
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 13, in <module>
    print(10/n)
ZeroDivisionError: division by zero
- 可以看到,当不指定输出,因为默认等级就是'WARNING'所以'info'和'debug'就无法正常输出了

- pdb


  • 第四种方式就是启动python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态
  • 如果有人用过ansible的话,ansible-playbook --step也可以单步执行剧本
  • 下面来看案例:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
s = '0'
n = int(s)
print(10 / n )
#执行: python -m pdb .py文件名称
PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe -m pdb f:/MY_python/Python/test2.py
> f:\my_python\python\test2.py(3)<module>()  #进入调试器
-> s = '0'    #第一行代码
(Pdb) l    #使用 ' l '可以查看当前所有代码 
  1     #!/usr/bin/env python3
  2     # -*- coding: utf-8 -*-
  3  -> s = '0'
  4     n = int(s)
  5     print(10 / n )
[EOF]
(Pdb) n  #使用' n '继续下一步 
> f:\my_python\python\test2.py(4)<module>()
-> n = int(s)
(Pdb) n
> f:\my_python\python\test2.py(5)<module>()
-> print(10 / n )
(Pdb) n
ZeroDivisionError: division by zero
> f:\my_python\python\test2.py(5)<module>()
-> print(10 / n )
(Pdb) p n   #使用' p 变量名 '可以查看当前变量的值
0
(Pdb) p s
'0'
(Pdb) n
ZeroDivisionError: division by zero
> <string>(1)<module>()->None
(Pdb) q   #使用' q '退出程序
PS F:\MY_python\Python> 
  • 使用pdb方法再命令行调试理论上是万能的,但是如果代码有好几千行,使用pdb方法显然就不太可以了


- pdb.set_trace()


这个方法也是使用pdb,但是无需单步执行,我们只需要在代码导入模块import pdb即可,然后再可能会出错的地方放一个pdb.set_trace(),就可以设置一个断点,例如:


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pdb
s = '0'
n = int(s)
pdb.set_trace()   #再运行到这里时会暂停,并且进入pdb调试环境
print(10 / n )
#执行
PS F:\MY_python\Python> & C:/Users/12488/AppData/Local/Microsoft/WindowsApps/python3.10.exe f:/MY_python/Python/test2.py
> f:\my_python\python\test2.py(8)<module>()
-> print(10 / n )
(Pdb) p n  #可以使用' p 变量名 ' 查看值
0
(Pdb) c  #继续运行剩余代码
Traceback (most recent call last):
  File "f:\MY_python\Python\test2.py", line 8, in <module>
    print(10 / n )
ZeroDivisionError: division by zero


  • 这样的方式虽然比直接使用pdb效率好一点,但是也是二斤八两


- IDE


  • 除了上面的调试方法,我们还可以直接下载一个支持调试功能的IDE,目前较好的有:VS codePyCharm,我自己使用的就是VS code


四、单元测试


如果你听过测试驱动开发(TDD:Test-Driven Development),那么单元测试就不会陌生


注释:测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统 软件开发流程 的新型的开发方法。 它要求在 编写 某个 功能 的 代码 之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。


单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验测试工作的,例如:

- 以'abs()'函数为例,我们来写几个测试用例:
1、输入正数,例如1、1.2、0.99,期待返回值与输入相同
2、输入负数,例如-1、-1.2、-0.99,期待返回值与输入相反
3、输入0,期待返回0
4、输入非数值类型,比如None、列表、字典等,期待抛出错误TypeError

把上述的测试用例放到一个测试模块里,就是一个完整的单元测试


如果单元测试通过,说明测试的函数能够正常工作,如果单元测试不通过,那么就需要看函数是不是又bug,是不是测试条件输入不正确,最终需要使测试单元能够成功通过


单元测试通过后,如果我们对abs()函数做了修改,只需要再跑一遍单元测试,如果通过,说明我们进行的修改没有对函数原有的行为造成影响,反之,测试不通过,就说明我们进行的修改对函数原有的行为造成了影响,这个时候就需要根据需求修改函数或者修改测试


这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例,在将来修改时,可以极大程度的保证该模块的行为仍然是正确的


这里不细说了,感兴趣的可以去看一下原文,原文讲述了如何进行单元测试,以及单元测试框架unittest的简单使用


五、文档测试


  • 如果经常阅读Python的官方文档,会看到很多文档都会有示例代码,例如:


>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef') 
>>> m.group(0) 
'def'


在自己动手在交互环境执行时,会发现输出结果是一样的,这些代码与其他说明可以写在注释中,然后又一些工具自动生成文档


既然这些代码本身就可以粘贴出来直接运行,那么是不是也可以自动执行写在注释中的这些代码呢,肯定是可以的,例如:

- 当我们编写注释时,如果写上这样的注释
def abs(n):
    '''
   获取数字绝对值的函数。
    示例:
    >>> abs(1)
    1
    >>> abs(-1)
    1
    >>> abs(0)
    0
    '''
    return n if n >= 0 else (-n)


像上面那样编写注释,无疑是更加明确的告诉函数调用者该函数的期望输入与输出


在Python中,Python内置的文档测试doctest模块可以直接提取注释中的代码并且执行测试


doctest严格按照Python交互式命令行的输入和输出,从而来判断测试结果是否正确,只有测试异常时,可以使用...表示中间一大段无用的输出,现在使用doctest来测试Dict类

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.
    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
    def __setattr__(self, key, value):
        self[key] = value
if __name__=='__main__':
    import doctest  #导入模块
    doctest.testmod()
- 没有任何输出,这说明我们编写的doctest运行都是正确的,如果程序有问题,例如修改一下'__getattr__()'方法,再次运行
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.
    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)
    def __getattr__(self, key):
        try:
            pass
            #return self[key]   #注释返回值
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
    def __setattr__(self, key, value):
        self[key] = value
if __name__=='__main__':
    import doctest
    doctest.testmod()
#执行输出:
**********************************************************************
File "f:\MY_python\Python\test2.py", line 9, in __main__.Dict
Failed example:
    d1.x
Expected:
    100
Got nothing
**********************************************************************
File "f:\MY_python\Python\test2.py", line 15, in __main__.Dict
Failed example:
    d2.c
Expected:
    '3'
Got nothing
**********************************************************************
File "f:\MY_python\Python\test2.py", line 21, in __main__.Dict
Failed example:
    d2.empty
Expected:
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
Got nothing
**********************************************************************
1 items had failures:
   3 of   9 in __main__.Dict
***Test Failed*** 3 failures.


  • 注意

注意上面的最后三行代码,当模块正常导入时,doctest不会被执行,只有在命令行直接运行时,才会执行doctest,所以不用担心dotest会在非测试环境下执行


  • 下面来看一个编写doctest的案例:
# -*- coding: utf-8 -*-
def fact(n):
    '''
    Calculate 1*2*...*n
    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest __main__.fact[2]>", line 1, in <module>
        fact(-1)
      File "/app/main.py", line 16, in fact
        raise ValueError('valueError!')
    ValueError: valueError!
    '''
    if n < 1:
        raise ValueError('valueError!')
    if n == 1:
        return 1
    return n * fact(n - 1)
if __name__ == '__main__':
    import doctest
    doctest.testmod()
- 执行后没有任何输出,其实只需要看报错信息修改注释即可,例如:
报错信息是这样的:
**********************************************************************
File "/app/main.py", line 10, in __main__.fact
Failed example:
    fact(-1)   #这是执行的语句
Exception raised:     #这个语句块下面的就是输出的结果,这个结果跟注释中写的不一样就会报错,只需要根据这个报错修改代码,或者修改注释即可
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest __main__.fact[2]>", line 1, in <module>
        fact(-1)
      File "/app/main.py", line 17, in fact
        raise ValueError('valueError!')
    ValueError: valueError!
**********************************************************************
1 items had failures:
   1 of   3 in __main__.fact
***Test Failed*** 1 failures.
修改代码后:
# -*- coding: utf-8 -*-
def fact(n):
    '''
    Calculate 1*2*...*n
    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest __main__.fact[2]>", line 1, in <module>
        fact(-1)
      File "/app/main.py", line 17, in fact
        raise ValueError('valueError!')
    ValueError: valueError!
    '''
    if n < 1:
        raise ValueError('valueError!')
    if n == 1:
        return 1
    return n * fact(n - 1)
if __name__ == '__main__':
    import doctest
    doctest.testmod()
#再次执行,发现语句没有输出了                     


目录
相关文章
|
1天前
|
测试技术 Go Python
在python中测试应用
【6月更文挑战第29天】本文介绍Python的unittest是内置的单元测试框架,适合线性控制流的代码测试。并举实例说明,如何组织测试代码,如何构造脚手架和测试套件。
16 6
在python中测试应用
|
4天前
|
XML 测试技术 数据格式
软件测试之 自动化测试 基于Python语言使用Selenium、ddt、unitTest 实现自动化测试(下)
软件测试之 自动化测试 基于Python语言使用Selenium、ddt、unitTest 实现自动化测试(下)
10 3
|
4天前
|
存储 前端开发 JavaScript
VSCode调试揭秘:Live Server助力完美测试Cookie与Session,远超“Open in Browser“!
VSCode调试揭秘:Live Server助力完美测试Cookie与Session,远超“Open in Browser“!
|
4天前
|
Java 测试技术 程序员
软件测试之 自动化测试 基于Python语言使用Selenium、ddt、unitTest 实现自动化测试(上)
软件测试之 自动化测试 基于Python语言使用Selenium、ddt、unitTest 实现自动化测试(上)
14 1
|
11天前
|
测试技术 Python
Python教程:利用timeit模块对代码进行性能测试
在Python中,了解代码的性能是优化和改进的关键。timeit模块是Python标准库中的一个工具,用于测量代码片段的执行时间。本文将介绍timeit模块的各种接口、命令行使用方法以及如何对代码中的函数或类进行性能测试。
21 3
|
12天前
|
存储 自然语言处理 数据可视化
在python中的内建函数和辅助工具进行调试
【6月更文挑战第18天】本文介绍Python的调试,涉及对代码的字节码和语法结构进行深入分析。通过这些工具,开发者能更好地理解和调试代码执行流程。
36 2
|
1天前
|
消息中间件 缓存 中间件
【赠书活动 - 第1期】- 测试工程师Python开发实战(异步图书出品)| 文末送书
【赠书活动 - 第1期】- 测试工程师Python开发实战(异步图书出品)| 文末送书
|
2天前
|
测试技术
Appium+python自动化(三十九)-Appium自动化测试框架综合实践 - 代码实现(超详解)
Appium+python自动化(三十九)-Appium自动化测试框架综合实践 - 代码实现(超详解)
|
2天前
|
测试技术 Python
python接口自动化测试 - unittest框架suite、runner详细使用
python接口自动化测试 - unittest框架suite、runner详细使用
|
3天前
|
安全 测试技术 Python
python单元测试
python单元测试