人生苦短,我用Python![1]
这个系列是一个帮助零基础的人入门编程的教程,本文主要介绍Python中的单元测试和异常处理。
测试
在软件开发过程中,测试(Testing)是非常重要的一个步骤,用于确保程序的正确性和质量。也能在最终部署之前,识别和纠正程序的错误和缺陷。
Fig. 1. 重大的程序bug会造成严重后果。Adapted from [2][3]
一些致命的软件bug会造成严重的后果,比如737MAX飞机的故障和Uber自动驾驶系统的故障,如图1所示[2][3]。除此之外,其他一些给社会造成严重负面影响的软件bug可以看看这里 (List_of_software_bugs) 整理的。为了避免这种情况,我们需要在程序中添加测试。
测试分为4个层次,分别是:
- 单元测试(Unit testing)
- 程序中单个单元或者组件会被测试
- 确保每一个单元运行良好,不会有任何错误
- 集成测试(Integration testing)
- 多个单元会被组合,并且作为一个集合被测试
- 在集成了多个单元之间的互动中,错误可能会被暴露
- 系统测试(System testing)
- 整个系统会作为一个整体被测试
- 确保达到所有的功能和要求
- 验收测试(Acceptance testing)
- 完整的程序会在用户部署之前进行测试
- 评估这个系统符合所有的业务要求
在这里我们主要重点关注如何给Python写单元测试,其他类型的测试是软件工程部分的内容。
那么要如何写测试呢?最基本的想法是,先考虑定义一个优秀的测试策略,其中包含很多不同的测试用例,这对于确保程序的正确性很重要。
那么一个优秀的测试策略:
- 要保证程序所有的功能都能在有限的时间内被覆盖并且测试
- 由合理并且可管理可维护的测试用例组成
- 最大化检测错误或者缺陷的可能性
举个例子,考虑有一个程序需要通过输入的分数来计算这个学生有没有通过考试。
1 | if mark >= 50 and mark <= 100: |
有效(正面)用例:
- 基于正确的输入数据
- 比如: 55, 60, 65, …, 85, 90, 95, …
无效(负面)用例:
- 基于错误的输入数据
- 比如: -1, 0, 5, …, 45, 49, 101, 200, …
边缘用例:
- 有效用例中的一些边界值
- 比如: (49, 50)和(100, 101)
Debug
Debug是指计算机程序中查找和解决缺陷或者问题的过程。在编写程序或者测试遇到bug时,就需要debug来解决。
在Python中,有两个比较基础的手段:
print
语句assert
语句
关于print
我们已经很熟悉了,它会输出这个变量的值,方便观测某个变量在运行时的值。
至于assert
:
- 它能检查一个表达式是否为真,如果不为真,就会抛出一个
AssertionError
异常 - 语法:
assert (condition), "<error_message>"
比如:
1 | assert size <= 5, "size should not exceed 5" |
但是总之,最好的debugging是实打实的理解你写的程序。
Python中的单元测试
在Python中,我们需要使用unittest
标准库来进行测试。
- 通过继承
unittest.TestCase
创建一个测试类(test class) - 在测试类中定义一个或多个测试方法(test method)
假设我们有一个函数product_func
被定义,并且需要为它编写单元测试。
1 | def product_func(first_arg, second_arg): |
那么单元测试可以这么写:
1 | import unittest |
运行这个测试时,将会有可能性: OK, FAIL, ERROR。
如果在Jupyter Notebook中,我们可以这样运行测试。
1 | suite = unittest.TestLoader().loadTestsFromTestCase(TestForProduct) |
在unittest
中,Python提供了以下assert的方法:
Method | Checks that | New in |
---|---|---|
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 |
3.1 |
assertIsNot(a, b) | a is not b |
3.1 |
assertIsNone(x) | x is None |
3.1 |
assertIsNotNone(x) | x is not None |
3.1 |
assertIn(a, b) | a in b |
3.1 |
assertNotIn(a, b) | a not in b |
3.1 |
assertIsInstance(a, b) | isinstance(a, b) |
3.2 |
assertNotIsInstance(a, b) | not isinstance(a, b) |
3.2 |
关于更全面的unittest
介绍和解释,请参考这个页面[4]。
Python中的错误和异常
编程中,错误(Error)一般分为以下3类:
- 语法错误(Syntax erros)
- 代码在语法上有问题,编译器/解释器无法理解代码
- Python中的例子:
SyntaxError
- 运行时错误(Runtime errors)
- 代码在运行时发生了错误,可以进行适当处理
- Python中的例子:
ValueError
,TypeError
,NameError
- 逻辑错误(Logic errors)
- 程序逻辑的实施不正确
- 程序运行不出错,但是结果是错误的
以下我们举一些Python例子来说明。
SyntaxError
:
- 程序中语法错误
1 | if a_number > 2 |
NameError
:
- 在程序中使用了一个未定义的变量或者模块
1 | a_number = random.random() |
TypeError
:
- 尝试使用不兼容的对象类型
1 | if a_number > 2: |
ValueError
:
- 尝试传入一个参数,类型正确但是值错误
1 | sum_of_two = int('1') + int('b') |
Python的异常处理
在Python中,主要使用try
和except
关键词来处理异常。
try
和except
:
- 在
try
块中的语句会被执行,如果没有发生异常,except
代码块会被跳过 - 如果发生了异常,并且满足
except
中的条件,那么对应的except
代码块会被执行 - 如果发生了异常,但是没有
except
满足条件,那么程序依然会报错退出
以下是一个例子,如果输入了非数字的字符串,或者除数为0,都可以进入到对应的except
代码块中进行处理。
1 | try: |
else
:
- 如果没有发生异常,
else
代码块会被执行 - 如果需要在没有发生异常的时候运行某些代码,这个就会很有用
例子:
1 | file_name = "input_file.txt" |
finally
:
- 作为一个清理用的代码块
- 无论是否发生异常,都会执行的代码块
1 | file_name = "input_file.txt" |
实践试试
这次的练习,我们来试试用一种能处理所有异常的方式来编写代码,并且还需要为程序写单元测试。如果有必要,还需要增加注释来帮助开发。
Debugging
我们需要写一个计算学生GPA的程序。每一个学生需要计算5门课的成绩,计算规则如下:
1 | A ----- 4.0 |
这里有一个有错误的代码,我们需要修复它。
1 | number_of_courses = 3 |
来找找看哪里错了吧。
首先是number_of_courses
应该是5而不是3。需要加一个assert
来检查这个变量不会出错。
1 | number_of_courses = 5 |
然后是有一个TypeError
因为len(number_of_courses)
中的len
需要接受一个集合类型而不是一个数字。
1 | for i in range(number_of_courses): |
然后input_grade.append
是一个方法,需要去调用这个方法,而不是赋值。
其次在字符串的组合中,(i+1)
需要被转换成字符串,因为字符串不能和数字相加。
而且input_grade
有一个NameError
因为它并没有被定义。
所以这一行会改成
1 | course_grades.append(input("Please enter the grade for " + str(i+1) + " unit: ")) |
接下来input_grades
也有NameError
错误,需要使用正确的变量名。同样对于grade
也是。
然后有一个逻辑错误,用户可能输入一个小写字母,而这个就会跳过以下代码块的处理。这里应该改为
1 | for grade in course_grades: |
测试
在上一节中,我们已经编写了一个简单的程序,用来计算学生的GPA。想一想对于上面这个程序,应该如何编写测试来检查潜在错误。来试试看写一些单元测试。
首先我们把以上程序放入一个函数中。
1 | def calculate_GPA(grade_list): |
接下来来编写单元测试。
1 | import unittest |
在Jupyter Notebook中,我们需要运行以下代码来运行测试。
1 | test = TestForGPA() |
异常处理
让我们改善Python编程基础05的实践题代码。
以下是原来的代码。
1 | # Open simple_file.txt for reading |
需要处理simple_file.txt
中的文本,文件的每一行中包含三个值,前两个是数字,第三个是表示数字的英文字符串,比如说“one”。我们需要做的是将第三个值转换为数字,并且将整个文件的结果写入到output_file.txt
里,我们只拿1~5作为例子。
1 | 1 2 five |
经过了这次的学习之后,我们可以用try-catch代码块来更好的处理异常错误。以下是一种参考范例。
1 | try: |
系列总结
至此,Python编程基础的内容就已经全部被覆盖完毕了。以上一共8篇blog覆盖了Python基础相关的大多数语法和编程思路。相信大家在学完以上8篇之后,就已经能够独立的编写Python程序而不会遇到很大障碍了。至于今后的内容,打算为一些Python高级语法再编写几篇后日谈,其中可能会包括类型注释,metaclass,装饰器,函数式编程和并发并行等等。
参考文献
- [1] B. Eckel, “sebsauvage.net - Python”, Sebsauvage.net, 2021. [Online]. Available: http://sebsauvage.net/python/.
- [2] C. Nast, "Boeing Plans to Fix the 737 MAX Jet With a Software Update", Wired, 2022. [Online]. Available: https://www.wired.com/story/boeing-737-max-8-ethiopia-crash-faa-software-fix-lion-air/.
- [3] "So who's to blame when a driverless car has an accident?", Abc.net.au, 2022. [Online]. Available: https://www.abc.net.au/news/2018-03-20/uber-driverless-car-accident-who-is-to-blame/9567766.
- [4] "unittest — Unit testing framework — Python 3.10.5 documentation", Docs.python.org, 2022. [Online]. Available: https://docs.python.org/3/library/unittest.html.
- [5] "Built-in Exceptions — Python 3.10.5 documentation", Docs.python.org, 2022. [Online]. Available: https://docs.python.org/3/library/exceptions.html.