Python编程:从入门到实践(学习笔记-Charpter 11)

Python编程:从入门到实践(学习笔记-Charpter 11)

十月 17, 2019

Chapter 11:测试代码

11.1 测试函数

name_function.py

1
2
3
4
def get_formatted_name(first, last):
"""Generate a neatly formatted full name."""
full_name = first + ' ' + last
return full_name.title()

names.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from name_function import get_formatted_name

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("\tNeatly formatted name: " + formatted_name + '.')

11.1.1单元测试和测试用例

Python 标准库中的模块 unittest 提供了代码测试工具。

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

测试用例:是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。

全覆盖式测试:用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

11.1.2可通过测试

test_name_function.py

1
2
3
4
5
6
7
8
9
10
11
12
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()

首先,我们导入了模块unittest 和要测试的函数get_formatted_name() 。在❶处,我们创建了一个名为 NamesTestCase的类,用于包含一系列针对get_formatted_name()的单元测试。你可随便给这个类命名,但最好让它看起来与要测试的函数相关,并包含字样Test。这个类必须继承unittest.TestCase类,这样Python才知道如何运行你编写的测试。

NamesTestCase 只包含一个方法,用于测试 get_formatted_name() 的一个方面。我们将这个方法命名为 test_first_last_name(),因为我们要核实的是只有名和姓的姓名能否被正确地格式化。我们运行 testname_function.py 时,所有以 test 打头的方法都将自动运行。在这个方法中,我们调用了要测试的函数,并存储了要测试的返回值。在这个示例中,我们使用实参’janis’和’joplin’调用get_formatted_name(),并将结果存储到变量formatted_name 中(见❷)。

在❸处,我们使用了 unittest 类最有用的功能之一:一个断 方法。断言方法用来核实得到的结果是否与期望的结果一致。在这里,我们知道 get_formatted_name() 应返回这样的姓名,即名和姓的首字母为大写,且它们之间有一个空格,因此我们期望 formatted_name 的值为 Janis Joplin 。为检查是否确实如此,我们调用 unittest的方法 assertEqual() ,并向它传递 formatted_name 和 ‘Janis Joplin’ 。代码行 self.assertEqual(formatted_name, ‘Janis Joplin’) 的意思是说: “ 将 formatted_name 的值同字符串 ‘Janis Joplin’ 进行比较,如果它们相等,就万事大吉,如果它们不相等,跟我说一声! ”

11.1.3 不能通过的测试

name_function.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_formatted_name(first, middle, last):
""" 生成整洁的姓名 """
full_name = first + ' ' + middle + ' ' + last
return full_name.title()


-------结果
❶E
======================================================================
❷ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
❸ Traceback (most recent call last):
File "test_name_function.py", line 8, 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.000s
❺FAILED (errors=1)

其中包含的信息很多,因为测试未通过时,需要让你知道的事情可能有很多。第 1 行输出只有一个字母 E (见❶),它指出测试用例中有一个单元测试导致了错误。接下来,我们看到 NamesTestCase 中的 test_first_last_name() 导致了错误(见❷)。测试用例包含众多单元测试时,知道哪个测试未通过至关重要。在❸处,我们看到了一个标准
的 traceback ,它指出函数调用 get_formatted_name(‘janis’, ‘joplin’) 有问题,因为它缺少一个必不可少的位置实参。

我们还看到运行了一个单元测试(见❹)。最后,还看到了一条消息,它指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误(见❺)。这条消息位于输出末尾,让你一眼就能看到 —— 你可不希望为获悉有多少测试未通过而翻阅长长的输出。

11.1.4 测试未通过时怎么办

试未通过时怎么办呢?如果你检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。

在这个示例中, get_formatted_name() 以前只需要两个实参 —— 名和姓,但现在它要求提供名、中间名和姓。新增的中间名参数是必不可少的,这导致 get_formatted_name() 的行为不符合预期。就这里而言,最佳的选择是让中间名变为可选的。这样做后,使用类似于 Janis Joplin 的姓名进行测试时,测试就会通过了,同时这个函数还能接受中间名。下面来修改 get_formatted_name() ,将中间名设置为可选的,然后再次运行这个测试用例。如果通过了,我们接着确认这个函数能够妥善地处理中间名。
要将中间名设置为可选的,可在函数定义中将形参 middle 移到形参列表末尾,并将其默认值指定为一个空字符串。我们还要添加一个 if 测试,以便根据是否提供了中间名相应地创建姓名:

name_function.py

1
2
3
4
5
6
7
def get_formatted_name(first, last, middle=''):
""" 生成整洁的姓名 """
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
11.1.5 添加新测试

确定 get_formatted_name() 又能正确地处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,我们在 NamesTestCase 类中再添加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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')

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()

我们将这个方法命名为 test_first_last_middle_name() 。方法名必须以 test_ 打头,这样它才会在我们运行 test_name_function.py 时自动运行。这个方法名清楚地指出了它测试的是 get_formatted_name() 的哪个行为,这样,如果该测试未通过,我们就会马上知道受影响的是哪种类型的姓名。在 TestCase 类中使用很长的方法名是可以的;这些方法的名称必须是描述性的,这才能让你明白测试未通过时的输出;这些方法由 Python 自动调用,你根本不用编写调用它们的代码。
为测试函数 get_formatted_name() ,我们使用名、姓和中间名调用它(见❶),再使用 assertEqual() 检查返回的姓名是否与预期的姓名(名、中间名和姓)一致。我们再次运行 test_name_function.py 时,两个测试都通过了

11.2 测试类

11.2各种断言方法

表 11-1 unittest Module 中的断言方法

方法 用途
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 中
11.2.2 一个要测试的类

survey.py

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

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

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

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

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

为证明AnonymousSurvey类能够正确地工作,我们来编写一个使用它的程序:

language_survey.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from survey import AnonymousSurvey

#定义一个问题,并创建一个表示调查的AnonymousSurvey对象
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

#显示问题并存储答案
my_survey.show_question()
print("(Enter 'q' to any time to quit). \n")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)

#显示调查结果
print("\nThink you to everyone who participated in the survey!")
print(" ")
my_survey.show_results()
11.2.3 测试 AnonymousSurvey 类

test_survey.py

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
import unittest
from survey import AnonymousSurvey

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

def test_store_single_response(self):
"""测试单个答案被妥善地存储"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
my_survey.store_response('English')

self.assertIn('English', my_survey.responses)

def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
responses =['English', 'Spanish', 'Mandarin']
for response in responses:
my_survey.store_response(response)

for response in responses:
self.asserIn(response, my_survey.responses)


unittest.main()
11.2.4 方法 setUp()
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
import unittest
from survey 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()