第十一章
编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。
1、测试函数
下面是要用于测试的函数,它接受名和姓并返回整洁的姓名,存储在文件name_function.py中:
def get_formatted_name(first, last):
full_name = first + ' ' + last
return full_name.title()
函数将名和姓合并成姓名,在名和姓之间加上一个空格,并将它们的首字母都大写,再返回结果。为核实函数是否像期望的那样工作,下面编写一个使用这个函数的程序:
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 + ".")
测试如下:
Enter 'q' at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin.
Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan.
Please give me a first name: q
从测试的结果可以知道,合并得到的姓名是正确的。如果现在需要添加处理中间名的功能,就需要在保证不破坏原来功能的基础上,添加新的功能,然后再进行测试。这样显得就太繁琐了,不过Python您提供了一种自动测试函数输出的高效方式,可对相应的函数进行自动测试。
(1)单元测试和测试用例
Python标准库中的模块unittest提供了代码测试工具。单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
(2)可通过的测试
要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。下面是只包含一个方法的测试用例:
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')
unittest.main()
# 输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
首先,需要导入模块unittest和要测试的函数。然后创建一个类,用于包含一系列针对被测试函数的单元测试,这个类必须继承unittest.TestCase类,这样Python才知道如何运行你编写的测试。
这个类只包含了一个方法,用于核实姓名能否被正确地格式化。在运行上述文件时,所有以test_大头的方法都将自动运行。
这里使用了unittest类中最有用的功能之一:一个断言方法。断言方法用来核实得到的结果是否与期望的结果一致。self.assertEqual()方法就是将第一个参数和第二个参数进行比较。
在输出的结果中,第一行的句点表明有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时间不到0.001秒。最后的OK表明该测试用例中的所有单元测试都通过了。
(3)不能通过的测试
我们故意只在函数中添加可以处理中间名的形参middle,而没有修改测试用例中的实参。运行结果:
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.002s
FAILED (errors=1)
由于测试没有通过,返回了很多信息。第一行输出只有一个字母E,它指出测试用例中有一个单元测试导致了错误。然后可以看到类中的函数导致了错误。当测试用例包含很多单元测试时,准确知道那个测试没通过至关重要。在往下,我们看到一个标准的traceback,它准确指出函数调用中出现了问题,因为它缺少了一个必不可少的位置实参。最后显示运行了一个单元测试,并指出整个测试用例都没有通过。
(4)测试未通过时怎么办
测试未通过说明你编写的新代码有错,此时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
对于上述未能通过的测试,我们知道是新增的中间名参数导致的,所以可以让中间名变为可选的,即添加默认值,然后再适当地添加if判断语句,就可以让测试通过了。
(5)添加新测试
下面再编写一个测试,用于测试包含中间名的姓名。
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')
def test_first_last_middle_name(self):
formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')
unittest.main()
# 输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
这个新添加方法的方法名必须以test_开头,这样它才会在我们运行整个测试文件时自动运行。在TestCase类中使用很长的方法名是可以的;这些方法名必须具有描述性的,这才能让你明白测试未通过时的输出;这些方法由Python自动调用,你根本不用编写调用它们的代码。
2、测试类
(1)unittest Module中常用的6个断言方法
方法 | 用途 |
---|---|
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中 |
Python在unittest.TestCase类中提供了很多断言方法。如果你认为应该满足的条件实际上并不满足,Python将引发异常。
(2)一个要测试的类
类的测试与函数的测死相似,但也存在一些不同之处。下面一个帮助管理匿名调查的类:
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)
要创建这个类的实例,只需提供一个问题即可。下面编写一个使用它的程序:
from survey import AnonymousSurvey
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
# 输出:
What language did you first learn to speak?
Enter 'q' at any time to quit.
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
(3)测试AnonymousSurvey类
下面编写一个测试:如果用户面对调查问题时只提供一个答案,这个答案也能被妥善地存储。
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
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)
unittest.main()
# 测试输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
这里的测试与测试函数时类似,这里的第一个方法验证调查问题的单个答案被存储后,会包含在调查结果列表中。要测试类的行为,需要创建其实例。由输出知道,测试顺利通过了。
下面来核实用户提供三个答案时,它们也将被妥善地存储。
def test_store_three_responses(self):
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
responses = ['English', 'Spanish', 'Mandarih']
for response in responses:
my_survey.store_response(response)
for response in responses:
self.assertIn(response, my_survey.responses)
# 测试输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
在测试类中添加上述的方法,有结果可知,测试也顺利通过了。但是这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。
(4)方法setUp()
在前面的示例中,我们在每个测试的方法中都创建了一个AnonymousSurvey实例,并在每个方法中都创建了答案。unittest.TestCase类中包含了方法setUp(),Python将先运行它,再运行各个以test_开头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp()中创建的对象。
下面使用setUp()来创建一个调查对象和一组答案,供两个测试方法使用:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
def setUp(self):
question = "What language did you first learn to speak?"
self.my_survey = AnonymousSurvey(question)
self.responses = ['English', 'Spanish', 'Mandarih']
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()
方法setUp()做了两件事情:创建一个调查对象;创建一个答案列表。存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用。
测试自己编写的类时,方法setUp()让测试方法编写起来更容易:可在setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。
注意:运行测试用例时,每完成一个单元测试,Python都打印一个字符:测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F。这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。