Python: 调试代码和单元测试

2022-09-20 14:16:55 浏览数 (1)

文章背景: 最近在学习华为云在线课程Python应用篇,其中有个章节是程序调试。在代码编写过程中,需要不断地调试代码,使其满足我们的开发要求。下面首先介绍程序调试的几种方法,然后介绍单元测试。

1 print语句

2 assert(断言)

3 断点调试

4 单元测试

4.1 单元测试的特殊方法

4.2 单元测试内置的条件判断

4.3 测试用例

1 print语句

用print语句调试代码是最简单的一种方法。在代码中合适的地方插入print语句,可以输出某些变量,方便查看。

代码示例:

代码语言:javascript复制
def foo(s):
    n = int(s)
    print('>>> n = %d' % n)
    return 10 / n

def main():
    foo('0')

main()
代码语言:javascript复制
>>> n = 0
ZeroDivisionError: division by zero

此方法的缺点是在程序上线时需要删除多余的print语句。

2 assert(断言)

assert(断言),是Python中用于调试的工具,依赖于内置变量__debug__,当其取值为True时assert才会执行。

assert expression [, arguments]

代码示例:

代码语言:javascript复制
def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

main()
代码语言:javascript复制
AssertionError: n is zero!

assert断言,表达式n != 0应该是True。如果断言失败,assert语句就会抛出AssertionError

启动Python解释器时可以用-O参数来关闭assert

将上述代码存入err.py文件中。在命令提示符中,进入err.py文件所在的文件夹,运行如下代码:

代码语言:javascript复制
python -O err.py

会得到错误提示:

代码语言:javascript复制
ZeroDivisionError: division by zero

在程序上线时一般会禁用断言。

3 断点调试

断点(Break point)是指在代码中指定位置,当程序运行到此位置时中断下来,开发者可查看此时各个变量的值。因断点中断的程序并没有结束,可以选择继续执行。

断点调试需要借助于IDE(如pycharm, VS code等);Python本身提供工具pdb,但在使用上不如IDE中的工具简单。

4 单元测试

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

比如对函数abs(),我们可以编写出以下几个测试用例:

  1. 输入正数,比如11.20.99,期待返回值与输入相同;
  2. 输入负数,比如-1-1.2-0.99,期待返回值与输入相反;
  3. 输入0,期待返回0
  4. 输入非数值类型,比如None[]{},期待抛出TypeError

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

(1) 如果单元测试通过,说明我们测试的这个函数能够正常工作;如果单元测试不通过,要么函数有bug,要么测试条件输入不正确。总之,需要修复使单元测试能够通过。

(2) 使用单元测试的好处是,如果我们后续对abs()函数代码做了修改,只需要再跑一遍单元测试。如果通过,说明我们的修改不会对abs()函数原有的行为造成影响;如果测试不通过,说明我们的修改与原有行为不一致,要么修改函数代码,要么修改测试代码。

4.1 单元测试的特殊方法

为了编写单元测试,我们需要引入Python自带的unittest模块。在unittest模块中,有以下几个常用的方法。

(1)unittest.main(): 执行测试用例;

(2)setUp(): 在每个测试方法执行之前执行。

若setUp()方法引发异常,测试框架会认为测试发生了错误,因此,测试方法不会被执行。

(3)tearDown(): 在每个测试方法执行之后执行。

setUp()方法成功运行,无论测试方法是否成功,都会运行tearDown()

(4)unittest.skip(reason): 无条件跳过(装饰器形式)。

(5)setUpClass()tearDownClass(): 在所有测试方法开始或结束的前后执行(类方法)。

4.2 单元测试内置的条件判断

下表总结了几种断言方法。

4.3 测试用例

(1)代码文件student.py的代码如下:

代码语言:javascript复制
class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def get_grade(self):
        if self.score > 100 or self.score < 0:
            raise ValueError('value must in 0 ~ 100')
        if self.score >= 80:
            return 'A'
        if self.score >= 60:
            return 'B'
        return 'C'

(2)测试代码文件student_test.py的代码如下:

代码语言:javascript复制
import unittest
from student import Student


class TestStudent(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        print("setUpClassn")

    @classmethod
    def tearDownClass(cls):
        print("tearDownClass")

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...n')

    def test_80_to_100(self):
        s1 = Student('Bart', 80)
        s2 = Student('Lisa', 100)
        self.assertEqual(s1.get_grade(), 'A')
        self.assertEqual(s2.get_grade(), 'A')

    def test_60_to_80(self):
        s1 = Student('Bart', 60)
        s2 = Student('Lisa', 79)
        self.assertEqual(s1.get_grade(), 'B')
        self.assertEqual(s2.get_grade(), 'B')

    def test_0_to_60(self):
        s1 = Student('Bart', 0)
        s2 = Student('Lisa', 59)
        self.assertEqual(s1.get_grade(), 'C')
        self.assertEqual(s2.get_grade(), 'C')

    def test_invalid(self):
        s1 = Student('Bart', -1)
        s2 = Student('Lisa', 101)
        with self.assertRaises(ValueError):
            s1.get_grade()
        with self.assertRaises(ValueError):
            s2.get_grade()


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

运行测试代码student_test.py,得到的结果如下:

代码语言:javascript复制
setUpClass

setUp...
tearDown...

setUp...
tearDown...

setUp...
tearDown...

setUp...
tearDown...

tearDownClass
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

(1) 编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

(2) 以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

(3) 对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。

(4) assertRaises方法可以用来确保一个特定的函数调用引发特定的异常,它可以通过上下文管理器(with语句)来包装内嵌代码。如果with语句中的代码引发了正确的异常,则测试通过;否则,测试失败。

参考资料:

[1] Python应用篇(https://education.huaweicloud.com/courses/course-v1:HuaweiX CBUCNXX123 Self-paced/about?isAuth=0&cfrom=hwc)

[2] Python调试代码的4种方法:print、log、pdb、PyCharm的debug(https://blog.csdn.net/xiemanR/article/details/72775737)

[3] 调试(https://www.liaoxuefeng.com/wiki/1016959663602400/1017602696742912)

[4] 单元测试(https://www.liaoxuefeng.com/wiki/1016959663602400/1017604210683936)

[5] unittest 中用于 skip 跳过 test method, test class,的相关装饰器(https://blog.csdn.net/HeatDeath/article/details/72633303)

[6] 测试面向对象程序(https://www.kancloud.cn/k12edu/python3_object_oriented/1161666)

0 人点赞