跳转至

第7章 调试与测试

7.1 调试方法

7.1.1 利用 print调试程序

  • 当程序输出结果与预期不一致时,在可能出错的位置利用print语句输出关键的变量,查看其取值与预期是否一致

  • 例:可变默认参数陷阱

1
2
3
4
def fun(values, lst=[]):
    for v in values:
        lst.append(v)
    return lst
1
 两次调用的参数完全相同,但输出是不一致的
1
fun([1, 2, 3])
1
[1, 2, 3]
1
fun([1, 2, 3])
1
[1, 2, 3, 1, 2, 3]
  • 可以怀疑问题出在lst参数上
1
2
3
4
5
def fun(values, lst=[]):
    print('lst:', lst)    # <--- 输出lst的值
    for v in values:
        lst.append(v)
    return lst
1
fun([1, 2])
1
2
3
4
5
6
7
lst: []





[1, 2]
1
fun([1, 2])
1
2
3
4
5
6
7
lst: [1, 2]





[1, 2, 1, 2]
  • 原因:可变参数的默认值也是一个变量,而且多次调用函数使用的是同一个默认参数

7.1.2 利用 logging调试程序

  • print的缺点
  • 调试的print语句需删除或注释掉
  • 与其他输出信息混杂在一块影响输出结果的判断
  • logging模块是Python内置的标准模块,主要用于输出运行日志,可以设置输出日志的等级、日志保存路径、日志文件回滚等
  • 可以通过设置不同的日志等级来对输出信息进行控制;
  • 可以定制输出信息的格式;
  • 可以方便地将不同类型的输出信息输出到不出的位置,例如文件

  • Logger对象

  • logging模块的功能主要通过Logger对象实现
  • Logger对象不用直接实例化,利用getLogger函数可以获取一个Logger对象
  • getLogger函数有一个参数name,用于指定Logger对象的名称。相同的name会返回同一个Logger对象
  • Logger对象的主要方法
  • fatal
  • critical
  • error
  • warn
  • info
  • debug

  • basicConfig用于配置Loger对象,主要参数包括

  • level:日志信息的等级
  • format:日志信息输出格式
  • 日志信息的等级
  • FATAL:致命错误;
  • CRITICAL:特别严重的错误,如内存耗尽、磁盘空间为空等,一般很少使用;
  • ERROR:一般的错误,如输入/输出操作失败;
  • WARNING:发生很重要的事件,但是并不是错误时,如用户登录密码错误
  • INFO:处理请求或者状态变化等日常事务
  • DEBUG:调试过程中使用DEBUG等级

  • 格式化字符串

属性名称 格式 说明
name %(name) Logger对象名
asctime %(asctime)s 精确到毫秒的日志事件时间
filename %(filename)s 日志文件名
pathname %(pathname)s 日志文件的全路径名称
funcName %(funcName)s 日志输出所在的函数
levelname %(levelname)s 日志的等级
levelno %(levelno)s 日志等级信息
lineno %(lineno)d 日志输出在代码中的行号
module %(module)s 日志输出所在的模块名
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import logging
logging.basicConfig(level=logging.INFO, # 日志级别为DEBUG
                    format='%(asctime)s - %(levelname)s - %(message)s - line: %(lineno)d')
logger = logging.getLogger()

def fun(values, lst=[]):
    logger.debug(lst)
    for v in values:
        lst.append(v)
    return lst

logger.info(fun([1, 2]))
logger.info(fun([1, 2]))
1
2
2021-08-15 11:24:42,088 - INFO - [1, 2] - line: 12
2021-08-15 11:24:42,090 - INFO - [1, 2, 1, 2] - line: 13
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import logging
logging.basicConfig(level=logging.DEBUG, # 日志级别为INFO
                    format='%(asctime)s - %(levelname)s - %(message)s - line: %(lineno)d')
logger = logging.getLogger()

def fun(values, lst=[]):
    logger.debug(lst)
    for v in values:
        lst.append(v)
    return lst

logger.info(fun([1, 2]))
logger.info(fun([1, 2]))
1
2
2021-08-15 11:24:42,868 - INFO - [1, 2] - line: 12
2021-08-15 11:24:42,869 - INFO - [1, 2, 1, 2] - line: 13

7.1.3 pdb调试器

  • pdb是Python内置的交互式调试器,它支持在源代码行级别设置断点、单步执行、列出源代码、查看变量值等功能
  • 运行pdb调试器的方法
  • 命令方式:在命令行中执行python -m pdb source_code.py
  • pdb.set_trace():在源代码中导入pdb模块,然后在代码中希望设置断点的位置放置pdb.set_trace()
  • breakpoint(): 使用方法与pdb.set_trace相似。但是,breakpoint是一个内置函数,不必导入pdb模块

  • pdb常用命令

命令 命令简写 功能
list l 列出代码
next n 单步执行,不进入函数内部
step s 单步执行,进入函数内部
where w 查看所在的位置
print p 输出变量值
args a 打印当前函数的参数
continue c 继续运行,直到遇到断点或者程序结束
return r 一直运行到函数返回
jump j 跳转到指定行数运行
break b 添加断点/列出断点
clearl cl 清除断点
disable d 禁用断点
enable -- 启用断点
tbreak -- 设置临时断点(断点处只中断一次)
condition -- 条件断点
help h 帮助
quit q 退出pdb

利用IDE调试

  • 常用的Python开发工具或IDE环境如PyCharm、Spyder、PyDev、VSCode等都有强大的调试功能,便用更加简单便捷

7.2 异常处理

7.2.1 异常的原因

  • 程序运行过程中由于没有考虑到的意外情况而引发的错误
  • 常见导致异常的原因
  • 数据类型不匹配、文件不存在、网络连接错误、除运算中分母为零、下标越界等
  • Python解释器在检测到异常之后,会根据异常的类型及异常信息,将其包装成一个异常对象
1
1/0
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-13-9e1622b385b6> in <module>
----> 1 1/0


ZeroDivisionError: division by zero
  • 主动抛出异常
  • raise
1
raise Exception  # 异常类
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------

Exception                                 Traceback (most recent call last)

<ipython-input-14-406393329809> in <module>
----> 1 raise Exception  # 异常类


Exception:
1
raise Exception('程序出现错误!')  # 异常对象
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------

Exception                                 Traceback (most recent call last)

<ipython-input-15-969c2ee3e5d6> in <module>
----> 1 raise Exception('程序出现错误!')  # 异常对象


Exception: 程序出现错误!

7.2.2 断言

  • assert语句
  • 用于判断一个表达式是否为真
  • 当表达式为True对程序的执行没有影响,当表达式False时触发AssertionError异常
  • 语法形式
    1
    assert 表达式 [, 异常信息]
    
    相当于
    1
    2
    if not expression:
        raise AssertionError
    
1
2
def fun(param):
    assert isinstance(param, str), '参数必须为字符串'
1
fun('abc')
1
fun(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-18-a0060b8a4c19> in <module>
----> 1 fun(1)


<ipython-input-16-c416ba3f78d7> in fun(param)
      1 def fun(param):
----> 2     assert isinstance(param, str), '参数必须为字符串'


AssertionError: 参数必须为字符串

7.2.3 异常处理

异常处理的过程

  • 异常处理基本方式
    1
    2
    3
    4
    try:
        语句块1
    except 异常类型:
        语句块2
    
  • 异常处理的过程
    • 首先,执行“语句块1”中的语句。
    • 若没有发生异常则跳过except子句,执行后续代码;
    • 若发生异常,则跳过“语句块1”的其余代码,执行except子句;
    • except子句判断“语句块1”中发生的异常的类型与“异常类型”是否一致
    • 若不一致则继续抛出异常,程序崩溃退出;
    • 若一致,则执行“语句块2”。“语句块2”中往往会输出错语信息,若有必要会中断程序运行,或者转而调用其他函数;
    • except子句执行完之后,继续执行后续代码。
1
2
3
4
5
6
def division(x, y):
    try:
        return x/y
    except ZeroDivisionError:
        print("除数为0")
    return 0
1
division(1, 2)
1
0.5
1
division(1, 0)
1
2
3
4
5
6
7
除数为0





0

获取异常实例

  • except子句中可使用as来获取异常类的实例
1
2
3
4
5
6
def division(x, y):
    try:
        return x/y
    except ZeroDivisionError as e:
        print(e,'\n',"除数为0")
    return 0
1
division(1, 0)
1
2
3
4
5
6
7
8
division by zero 
 除数为0





0

捕获多种异常

  • 下面的代码,可能触发的异常有ZeroDivisionErrorAssertionError
1
2
3
def division(x, y):
    assert y != 1
    return x/y
  • 使用更高级别的异常类
  • ZeroDivisionErrorArithmeticError的子类,而ArithmeticErrorException的子类;AssertionError也是Exception的子类
  • 这种方法往往也会捕获到预期之外的异常,从而使得程序中的相关错误无法被发现
1
2
3
4
5
6
def division(x, y):
    try:
        assert y != 1, '分母为1'
        return x/y
    except Exception as e:
        print(e)
1
division(1, 1)
1
分母为1
1
division(1, 0)
1
division by zero
  • 异常元组
1
2
3
4
5
6
def division(x, y):
    try:
        assert y != 1, '分母为1'
        return x/y
    except (AssertionError, Exception) as e:
        print(e)
  • 使用多个except子句
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def division(x, y):
    try:
        assert y != 1, '分母为1'
        return x/y
    except AssertionError as e:
        print(e)
        return x
    except Exception as e:
        print(e)
        return 0

else子句

1
2
3
4
5
6
 try:
      语句块1
 except 异常类型:
      语句块2
 else:
      语句块3
- 当“语句块1”中没有发生异常时,会执行else子句中的“语句块3”

1
2
3
4
5
6
7
8
9
while True:
    try:
        value = input("请输入一个数字:")
        value = float(value)
    except Exception as e:
        print("输入错语,请再次尝试!")
    else:
        print("输入正确!")
        break
1
2
3
4
5
6
7
8
请输入一个数字:
输入错语,请再次尝试!
请输入一个数字:
输入错语,请再次尝试!
请输入一个数字:
输入错语,请再次尝试!
请输入一个数字:1
输入正确!

finally子句

1
2
3
4
5
6
try:
      语句块1
 except 异常类型:
      语句块2
 finally:
      语句块4
- finally子句中的“代码块4”无论“语句块1”是否出现异常都会被执行

7.2.4 异常的类型

内置异常类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
BaseException                              # 所有异常类的基类
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception                             # 常规异常的基类
      +-- StopIteration                    # 可迭代对象终止
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError          # 浮点运算异常
      |    +-- OverflowError
      |    +-- ZeroDivisionError           # 除数为0异常
      +-- AssertionError
      +-- AttributeError                   # 对象属性异常
      +-- BufferError
      +-- EOFError                         # 文件访问终止
      +-- ImportError                      # 模块导入异常
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError                        # 对象未声明/初始化
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError           # 文件不存在
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError             
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

自定义异常

  • 自定义异常类通常派生自Exception类,并利用raise语句在满足一定条件的情况下主动触发
  • 大多数自定义异常类都仅用于确定程序错误的原因并显示异常信息,往往不需要定义复杂的功能
1
2
3
4
5
6
class ParameterException(Exception): 
    pass
def greeting(info):
    if not isinstance(info, str):
        raise ParameterException("参 数 必 须 为 字 符 串!") 
    print(info)
1
greeting("Hello Python")
1
Hello Python
1
greeting(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
---------------------------------------------------------------------------

ParameterException                        Traceback (most recent call last)

<ipython-input-33-70f534de3be9> in <module>
----> 1 greeting(1)


<ipython-input-31-cebce8c89763> in greeting(info)
      3 def greeting(info):
      4     if not isinstance(info, str):
----> 5         raise ParameterException("参 数 必 须 为 字 符 串!")
      6     print(info)


ParameterException: 参 数 必 须 为 字 符 串!

7.3 单元测试*

7.3.1 单元测试的概念及工具

  • 单元测试(Unit Testing)是软件开发中使用的一种重要的自动化测试方法,也是测试驱动开发的必要过程
  • 测试单元
  • 在单元测试中,每个测试单元仅关注一个较小的、独立的功能
  • 在面向过程编程中,测试单元可以是一个程序、函数或者过程
  • 在面向对象编程中,常常以方法作为测试单元。

常用单元测试框架

  • unittest
  • Python标准库中自带的单元测试框架
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('abc'.upper(), 'ABC')

    def test_loser(self):
        self.assertEqual('ABC'.lower(), 'abc')

if __name__ == '__main__':
    unittest.main()
  • nose
  • nose是Python的一种第三方测试框架
  • pip install nose
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import nose

def test_upper():
    assert 'abc'.upper() == 'ABC'

def test_loser():
    assert 'ABC'.lower() == 'abc'

if __name__ == '__main__':
    nose.runmodule()
  • py.test
  • pip install pytest
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pytest

def test_upper():
    assert 'abc'.upper() == 'ABC'

def test_loser():
  assert 'ABC'.lower() == 'abc'


if __name__ == '__main__':
    pytest.main(["-s","test_file_name.py"])

7.3.2 unittest基础

  • 测试用例(Test Case)
  • 测试用例是独立的测试流程。在测试用例中,向测试目标输入特定的数据,检查返回结果与预期是否一致来验证程序的正确性
  • 测试设施(Test Fixture)
  • 测试设施用于搭建和清理测试环境。测试用例中不同的测试方法可能会需要一些共用的资源,例如文件、数据库连接、输入数据等。测试设施在测试方法执行之前准备这些资源,并在测试方法运行结束后进行清理
  • 测试套件(Test Suite)
  • 是一组测试用例或者其他测试套件的集合
  • 测试加载器(Test Loader)
  • 用于从类和模型中创建测试套件
  • 测试运行器(Test Runner)
  • 负责执行测试并控制测试结果输出

  • unittest模块中常用的类或函数包括:

  • unittest.TestCase
    • 所有测试用例类的基类;
  • unittest.main()
    • 该函数可以将一个单元测试模块变为可直接运行的测试脚本,它使用TestLoader类来搜索所有包含在该模块中命名以test开头的测试方法并自动执行他们。执行的默认顺序是根据方法名的ASCII码顺序;
  • unittest.TestSuite
    • 测试套件类;
  • unittest.TextTestRunner
    • 该类中的run方法运行测试套件中的测试用例;
  • unittest.defaultTestLoader
    • 该类中的discover方法根据匹配条件自动搜索测试目录中的测试用例文件,并将查找到的测试用例组装为测试套件;
  • unittest.skip
    • 装饰器,用于屏蔽测试用例中的暂时不需执行的测试方法。

7.3.3 创建测试用例

  • 通过继承自unittest.TestCase类可创建一个测试用例

  • 文件math_method.py

1
2
3
4
5
class MathMethod():
    def add(self,a,b):
        return a+b
    def sub(self,a,b):
        return a-b
  • 文件test_case.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import unittest
from math_method import MathMethod
class TestMathNMethod(unittest.TestCase):
    def test_add_two_zero(self):
        res=MathMethod().add(0,0)
        print('两个0相加',res)
        self.assertEqual(0,res)

    def test_add_two_positive(self):
        res=MathMethod().add(1,8)
        print('两个正数相加',res)
        self.assertEqual(9,res)

    def test_add_two_negative(self):
        res=MathMethod().add(-1,-4)
        print('两个负数相加',res)
        self.assertEqual(-5,res)
  • 常用断言方法
断言方法 功能
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)
assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds)抛出异常 exc
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertListEqual(a, b) 列表ab相等
assertTupleEqual(a, b) 元组ab相等
assertSetEqual(a, b) 集合ab相等
assertDictEqual(a, b) 字典ab相等

7.3.4 运行测试用例

  • 模块内执行
  • 在测试用例所在的模块中添加如下代码,直接运行模块

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

  • 命令行运行

  • python -m unittest test_module
    • 扩展名.py可以省去
  • python -m unittest test_module.TestCaseClass

7.3.5 测试套件的创建与执行

  • 测试代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import unittest
from unit_test import TestMathNMethod

suite = unittest.TestSuite()     # 测试套件
loader = unittest.TestLoader()   # 测试加载器
suite.addTest(loader.loadTestsFromTestCase(TestMathNMethod))

file = open('test_result.txt', 'w+')
runner = unittest.TextTestRunner(stream=file, verbosity=2)  # verbosity用于控制测试报告的详细程度
runner.run(suite)
  • 运行结果
1
2
3
4
5
6
7
8
test_add_two_negative (unit_test.TestMathNMethod) ... ok
test_add_two_positive (unit_test.TestMathNMethod) ... ok
test_add_two_zero (unit_test.TestMathNMethod) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK
  • 利用测试套件控制测试方法的执行顺序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import unittest
from unit_test import TestMathNMethod

suite = unittest.TestSuite()
tests = [
    TestMathNMethod('test_add_two_zero'),
    TestMathNMethod('test_add_two_negative'),
    TestMathNMethod('test_add_two_positive')
]

suite.addTests(tests)

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

7.3.6 测试设施

  • unittest中测试用例的测试设施通过方法setUptearDown实现
  • setUp方法在所有测试方法执行之前运行,用于准备所有测试方法共同的资源
  • tearDown方法在所有测试方法执行毕后执行,用于作一些清理工作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import unittest
from math_method import MathMethod

class TestMathNMethod(unittest.TestCase):
    def setUp(self):
        self.unit = MathMethod()

    def tearDown(self):
        del self.unit

    def test_add_two_zero(self):
        res = self.unit.add(0, 0)
        print('两个0相加', res)
        self.assertEqual(0, res)

    def test_add_two_positive(self):
        res = self.unit.add(1, 8)
        print('两个正数相加', res)
        self.assertEqual(9, res)

    def test_add_two_negative(self):
        res = self.unit.add(-1, -4)
        print('两个负数相加', res)
        self.assertEqual(-5, res)

7.4 文档测试

  • doctest
  • 是Python标准库自带的一种测试工具,可以用于简单的单元测试
  • 其工作原理是在函数或类的文档字符串中寻找测试用例并执行,比较输出结果与期望值是否相匹配

7.4.1 文档测试用例

  • 文档字符串中“>>>”符号之 后为待执行的测试代码,紧接着为测试代码的预期输出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def division(x, y):
    '''
    除法运算
    Args:
        x: 数值1
        y: 数值2

    Example:
    >>> division(1, 1)
    1.0
    >>> division(1, 0)
    除数为0
    0
    >>> division(2, 1)
    2.0
    '''
    try:
        return x/y
    except ZeroDivisionError:
        print("除数为0")
    return 0

7.4.2 运行文档测试

  • 模块内运行
  • 在模块中添加如下代码,即可运行文档测试
1
2
3
if __name__ == '__main__':
    import doctest
    doctest.testmod()
  • 命令行运行
  • python -m unittest doc_test_module
    • 扩展名.py不是必须的