调试和测试

阅读: 2749


请相信我,没有一次性写好的代码,也没有不用修改、优化、测试、扩展的代码!无论你事先多有把握,思考了多久,设计得多完善,总会存在各种各样,你意想不到的问题,或者说缺陷,甚至是Bug。这些都需要你在代码编写过程中或者后期维护升级中,不断的对程序进行调试和测试。

调试

有的问题简单,有的问题很复杂,但不管简单还是复杂,通常我们都无法一眼就能看出问题的具体性质。我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是错误的,某些关键的代码是否符合我们的预期,这些就需要我们将过程状态输出到屏幕或日志上,方便我们检查和分析。

1. 初学者简单粗暴的方法:print大法

print基本是初学者调试程序的自然选择,也是“陪伴你一生”的方法。

假设有下面的代码:

def func(s):
    return s/10

# 假设下面的调用语句在别的地方,无法直观的查看到。
func("100")

运行结果:

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/1.py", line 4, in <module>
    func("100")
  File "F:/Python/pycharm/201705/1.py", line 2, in func
    return s/10
TypeError: unsupported operand type(s) for /: 'str' and 'int'

对于有一定基础的同学来说,上面的代码,不用运行也能看出问题,但对于初学者,可能会经常碰到这类问题。于是,只能老老实实阅读调用错误栈的信息,发现这是个TypeError类型错误,提示s的类型有问题,自然而然的我们就会想到,那我看看s是什么类型的吧,用print打印一下。

def func(s):
    print(type(s))
    return s/10

# 假设下面的调用语句在别的地方,无法直观的查看到。
func("100")

虽然问题依然存在,但我们通过print()打印出了调用func函数时s的数据类型,是个字符串,字符串当然无法除以10。问题找到了,就可以设计相应的解决办法了。

也许有同学问,错误栈信息里不是有提示s是字符串类型吗?这是因为本例很简单,你能直接看到,但是大多数情况下,是需要你根据信息自己去查找具体原因的。

print()方法是新手最常用的方法,也是比较简单的方法。但是,用print()最大的问题是将来还得删掉它,而且代码里多了很多无用的行,想想程序里到处都是print(),运行结果也会包含很多垃圾信息,还影响执行效率,代码一点也不简洁优雅,就觉得不是那么愉快了。

2. 断言assert

Python内置了一个assert关键字,表示断言。凡是用print()来辅助查看的地方,都可以用断言来替代。它会对后面的表达式进行判断,如果表达式为True,那就什么都不做,程序接着往下走;如果False,那么就会弹出异常。比如下面的例子,断言此处a的值必定大于5,如果不是,那么说明前面的代码有问题,程序在此中断!

a = 1
pass
assert a > 5
pass

运行结果:

Traceback (most recent call last):
  File "F:/Python/pycharm/201705/1.py", line 3, in <module>
    assert a > 5
AssertionError

assert是一个非常有用的技巧,通过选择关键因子,对因子的状态进行判定,可以将程序一块一块的进行划分,逐步的缩小问题范围。

但是,程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert。关闭后,所有的assert语句相当于pass语句。

3. 日志logging

logging是Python内置的一个日志模块,不但可以将信息在屏幕上打印,还可以输出到文件,保留下来。使用logging之前,需要先通过import logging导入该模块。

具体的logging模块的用法,在后面的章节有详细的介绍,这里简单举例。

import logging

logging.basicConfig(level=logging.INFO)

def func(s):
    logging.info("s的数据类型为 %s" % type(s))
    return s/10

func("100")

运行结果:

INFO:root:s的数据类型为 <class 'str'>
Traceback (most recent call last):
  File "F:/Python/pycharm/201705/1.py", line 9, in <module>
    foo("100")
  File "F:/Python/pycharm/201705/1.py", line 7, in foo
    return s/10
TypeError: unsupported operand type(s) for /: 'str' and 'int'

logging允许你指定记录信息的级别,有DEBUG,INFO,WARNING,ERROR等几种级别,级别参数level会忽略比它低的级别信息。当指定level=INFO时,logging.DEBUG就不起作用了。同理,指定level=WARNING后,DEBUGINFO就不起作用了。logging.basicConfig(level=logging.INFO)这行就是指定只有INFO以上级别的信息才会被记录下来。

4. pdb模块

除了上面的方法外,Python还专门提供了一个pdb模块,可以单步或断点调试程序。运行方式:$ python -m pdb 文件名。但这依然不是好的解决方案,因为它不够简单直观。我们通常更多使用的还是IDE的调试功能,比如Pycharm的调试功能。

5. Pycharm调试方法

下面的代码本身没有问题,只是通过它展示调试的方法:

total = 0

for i in range(10):
    total += 1

print(total)

注意观察下面的图片,上不工具栏中的绿色甲虫是调试启动的按钮。中部左边的红点是断点,表示该行被监控,程序会在此处暂停并打印相关信息,鼠标左键点击行号右边增加断点,再次点击取消断点。中部每行右边的绿色信息是提示信息,比如total:2,表示程序运行到此处的时候,total的值为2。界面左下方是调试控件窗口,其中的绿色三角图标是步进按钮,点击一下,程序执行一行。红色方块按钮是停止调试。

image.png-151.3kB

更详细的Pycharm调试功能就不在这里详述了,它只是IDE的使用方法,没有什么难掌握的,同学们可以自己研究一下。

单元测试之unittest

单元测试是用来对一个模块、一个函数或者一个类进行正确性检验的工作。你的代码可能在语法、词法和运行过程中没有问题了,但是并不能代表它就完全符合你的设计预期,很有可能你希望得到A,它给你的结果却是B。这就需要我们对程序进行单元测试。单元测试测的不是语法问题,而是业务逻辑是否正确的问题。单元测试是软件开发过程中非常重要的一个环节。

Python中有太多的单元测试框架和工具,比如unittest、testtools、subunit、coverage和testrepository等等,并且运行单元测试也有很多种方法。由于它是测试工作而非功能实现,很多人都不太重视甚至忽略了单元测试。但是作为一个优秀的程序员,不仅要写好功能代码,写好测试代码一样重要。

单元测试具有以下特点:

  • 单元测试可以有效地测试某个程序模块的行为。

  • 单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。

  • 单元测试本身代码要简单,如果测试代码太复杂,那么测试代码本身都可能有bug。

  • 单元测试通过了并不意味着程序就完全OK了,但是不通过的程序肯定有问题。


这里简要介绍一下unittest,它是Python的标准模块之一,是其它测试框架和工具的基础,更多内容可以参考官方文档http://docs.python.org/3.6/library/unittest.html

比如下面的例子,就是一个对Python内置字符串类型的测试用例:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # 当测试参数不是字符串的时候,应该弹出类型异常
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

运行结果:

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

例子中用三个测试方法分别对字符串类型的三个内置方法的功能进行了测试。test_upper()用于测试字符串的upper()方法是否符合预期地将字符串全部大写了。test_isupper()用于测试字符串的isupper()方法是否能够准确判断大小写。test_split()用于测试字符串的split()方法分割字符串的功能。

编写单元测试时,需要编写一个测试类,这个类要继承unittest.TestCase。例子中,这个类叫做TestStringMethods。在这个类中,以test开头的方法就是测试方法,不以test开头的方法不是测试方法,测试的时候不会被执行。

unittest.TestCase类提供了很多内置的条件判断,只需要调用这些方法就可以断言输出是否是所期望的。最常用的断言就是assertEqual()

self.assertEqual('foo'.upper(), 'FOO')  # 断言执行'foo'.upper()后的结果是'FOO'

另一种重要的断言就是期待抛出指定类型的Error,比如使用非字符串参数对字符串进行分割时,断言会抛出TypeError:

with self.assertRaises(TypeError):
    s.split(2)

一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在代码的最后加上:

if __name__ == '__main__':
    unittest.main()

这样我们就可以在命令行下运行脚本,或者直接在Pycharm中运行。如果在运行时提供-v参数,可以获得更详细的结果。

另一种方法是在命令行中通过参数-m unittest运行单元测试:

$ python -m unittest 测试脚本

这种方法的好处是可以一次批量运行很多单元测试,并且,有很多工具可以帮助自动运行这些单元测试。

setUp()与tearDown()

在单元测试中有两个特殊的方法:setUp()和tearDown()。这两个方法会分别在每调用一个测试方法的前后被执行。

setUp()和tearDown()方法有什么用呢?设想你的测试需要打开一个文件,并读取内容,这时,就可以在setUp()方法中打开文件读取内容,在tearDown()方法中关闭文件,这样就不必在每个测试方法中重复相同的代码:

class TestMyUnit(unittest.TestCase):

    def setUp(self):
        # 打开文件
        # 读取内容

    def tearDown(self):
        # 关闭文件

    psss


评论总数: 0