Python学习之路10-测试代码

《Python编程:从入门到实践》笔记。
本章主要学习如何使用Python标准库中的unittest模块对代码进行简单的测试。

1. 前言

作为初学者,并非必须为你尝试的所有项目编写测试;但参与工作量较大的项目时,你应对自己编写的函数和类的重要行为进行测试。这样你就能够更加确定自己所做的工作不会破坏项目的其他部分,你就能够随心所欲地改进即有代码。如果不小心破坏了原来的功能,你马上就会知道,从而能够轻松地修复问题。相比于等到Bug出现后再去改,在测试未通过时采取措施要容易得多。而且,如果你想要分享你的项目,有测试的代码更容易让人接受。

2. 测试函数

2.1 一个能通过的测试

以下是一个将用户输入的姓与名拼接的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# name_function.py
def get_formatted_name(first, last):
"""返回一个整洁的完整姓名"""
full_name = first + " " + last
return full_name.title()


if __name__ == '__main__':
print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == "q":
break
last = input("Please give me a last name: ")
if last == "q":
break

formatted_name = get_formatted_name(first, last)
print("\nNeatly formatted name: " + formatted_name + ".")

当然你也可以将if语句下面的代码单独放在一个文件中,并在该文件开头带入get_formatted_name()函数。

if __name__ == "__main__"的补充:

在Python中,模块就是对象,所有模块都有一个内置属性__name__,当该模块被导入时,该模块的__name__属性会被置为模块名,当直接运行该模块,或者说直接运行该文件时,该属性就会使用默认值"__main__",可以用一句经典的话总结这个用法:

Make a script both importable and executable.

if语句下面的代码相当于对上面的函数的测试,不过这样的测试每次都需要我们自己输入数据,并自己根据结果判断代码是否工作正常,如果代码稍微多一点,稍微复杂一点,这样的测试方法将会很繁琐,所以,我们使用unittest模块了测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 代码test_name_function.py:
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
"""测试name_function.py"""

def test_first_last_name(self):
"""能够正确地处理像Janis Joplin这样的名字吗?"""
formatted_name = get_formatted_name("janis", "joplin")
self.assertEqual(formatted_name, "Janis Joplin")

unittest.main()

# 结果:
. # 这里有个实心句点
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

这里先明确两个概念:

单元测试:用于核实函数在某个方面没有问题

测试用例:一组单元测试,这些单元测试一起核实函数在各种情况下的行为都符合要求。

也就是说,你可以将上述代码中的test_first_last_name看做单元测试,而将NamesTestCase看做测试用例。

一般测试文件单独放在一个文件夹中,也可以将测试都放在一个文件中。

为函数编写测试用例,可先导入unittest模块和要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。在测试用,我们使用断言self.assertEqual()(并不是只有这一个断言函数)来判断结果与期望是否相同。在测试类中的每一个测试方法都必须以test_开头,否则将不会被认定是一个单元测试。最后我们通过unittest.main()来运行这个文件中的所有测试。当测试通过时,结果中会先输出一个实心句点,输出几个句点表示通过了几个单元测试,然后输出单元测试数目,最后输出OK

2.2 一个不能通过的测试

外国人的名字还有中间名,以上代码并未考虑这个情况。我们通过将上述代码改成含有中间名的版本来演示测试不通过的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 代码:
def get_formatted_name(first, middle, last):
"""返回一个整洁的完整姓名"""
full_name = first + " " + middle + " " + last
return full_name.title()

# 其余代码均不变

# 运行上面测试代码后的结果:
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能够正确地处理像Janis Joplin这样的名字吗?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_name_function.py", line 10, in test_first_last_name
formatted_name = get_formatted_name("janis", "joplin")
TypeError: get_formatted_name() missing 1 required positional argument: 'last'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

第一行输出了一个字母Etraceback指出缺少了参数。如果你检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,测试未通过时,不是去修改测试代码,而失去修改你编写的代码。

2.3 添加新测试

以下我们将上述的get_formatted_name()函数修改为能自动处理中间名的函数,并在测试文件中添加一个单元测试:

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
# name_function.py
def get_formatted_name(first, last, middle=""):
"""返回一个整洁的完整姓名"""
if middle:
full_name = first + " " + middle + " " + last
else:
full_name = first + " " + last
return full_name.title()

# test_name_function.py
import unittest
from chapter11 import get_formatted_name

class NamesTestCase(unittest.TestCase):
"""测试name_function.py"""

def test_first_last_name(self):
"""能够正确地处理像Janis Joplin这样的名字吗?"""
formatted_name = get_formatted_name("janis", "joplin")
self.assertEqual(formatted_name, "Janis Joplin")

def test_first_last_middle_name(self):
"""能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
formatted_name = get_formatted_name("wolfgang", "mozart", "amadeus")
self.assertEqual(formatted_name, "Wolfgang Amadeus Mozart")

unittest.main()

# 结果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

3. 测试类

前面讲的都是对函数的测试,这里我们开始对类的测试。在测试之前,先介绍几种常用的断言方法:

方法 用途
assertEqual(a, b) 核实 a == b
assertNotEqual(a, b) 核实 a != b
assertTrue(x) 核实x为True
assertFalse(x) 核实x为False
assertIn(item, list) 核实item在list中
assertNotIn(item, list) 核实item不在list中

下面创建一个匿名调查类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# survey.py
class AnonymousSurvey:
"""收集匿名调查问卷的答案"""

def __init__(self, question):
"""存储一个问题,并为存储答案做准备"""
self.question = question
self.responses = []

def show_question(self):
"""显示调查问卷"""
print(self.question)

def store_response(self, new_response):
"""存储单份调查问卷"""
self.responses.append(new_response)

def show_results(self):
"""显示收集到的所有答卷"""
print("Survey results:")
for response in self.responses:
print("- " + response)

以下是对该类的测试代码:

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
# test_survey.py
import unittest
from chapter11 import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""

def setUp(self):
"""创建一个调查对象和一组答案,共测试方法使用"""
question = "What language did you first learn to speak?"
self.my_survey = AnonymousSurvey(question)
self.responses = ["English", "Spanish", "Mandarin"]

def test_store_single_response(self):
"""测试单个答案呗妥善地存储"""
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0], self.my_survey.responses)

def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)

unittest.main()

# 结果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

这里的setUp()方法相当于普通函数的__init__()方法,用于初始化这个测试类,减少重复代码,比如,如果不用setUp()方法,那么question变量在每个测试函数中都要声明一次,十分麻烦低效。你过测试类中包含了setUp()方法,Python将先运行它,再运行各个以test_开头的方法。

至此,Python的基础部分大致结束,后面将是项目部分,以后可能还会对基础部分进行补充。

VPointer wechat
欢迎大家关注我的微信公众号"代码港"~~
您的慷慨将鼓励我继续创作~~