第十一章——测试代码
在本章中, 你将学习如何使用Python模块unittest中的工具来测试代码。
11.1 测试函数
name_function.py
def get_formatted_name(first,last):
"""Generate a neatly formatted full name."""
full_name = first + ' '+ last
return full_name.title()
names.py
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input("Please input your first name: ")
if first == 'q':
break
last = input("Please input your last name:")
if last == 'q':
break
name = get_formatted_name(first,last)
print(name)
假设我们要修改get_formatted_name(),使其还能够处理中间名。确保不破坏这个函数处理只有名和姓的姓名的方式,为此,只能每次改动进行测试,但过于繁琐。
Python提供了一种自动测试函数输出的高效方式,Python标准库中的模块unittest提供了代码测试工具。
11.1.1 单元测试和测试用例
单元测试用于核实函数的某个方面没有问题(具有测试功能的模块)
测试用例是一组单元测试(进行测试的数据)
全覆盖式测试用例:包含一整套单元测试,涵盖了各种可能的函数使用方式。(包含各种函数使用方式的测试实例)
11.1.2 可通过的测试
要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。
import unittest
from name_function import get_formatted_name
class NameTest(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类最有用的功能之一:一个断言方法。断言方法用来核实得到的结果是否与期望的结果一致。
self.assertEqual(formatted_name,'Janis Joplin')
代码行unittest.main()让Python运行这个文件中的测试。
unittest.main()
运行结果如下:
第1行的句点表明有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时
间不到0.001秒。最后的OK表明该测试用例中的所有单元测试都通过了。
11.1.3 不能通过的测试
下面是函数get_formatted_name()的新版本,它要求通过一个实参指定中间名:
def get_formatted_name(first,middle,last):
"""Generate a neatly formatted full name."""
full_name = first + ' ' + middle + '' + last
return full_name.title()
对其进行测试,运行程序test_name_function.py时,输出如下:
有一个字母E,它指出测试用例中有一个单元测试导致了错误。
接下来,我们看到NamesTestCase中的test_first_last_name()导致了错误。测试用例包含众多单元测试时,我们可以在这里看到哪个测试发生错误。
下面我们看到了一个标准的traceback,它指出函数调用get_formatted_name('janis', 'joplin')有问题,因为它缺少一个必不可少的位置实参。
最后,还看到了一条消息,它指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误。这条消息位于输出末尾,让你一眼就能看到。
11.1.4 测试未通过时怎么办
测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
在这个示例中, get_formatted_name()以前只需要两个实参——名和姓,但现在它要求提供
名、中间名和姓,最佳的选择是让中间名变为可选的。
def get_formatted_name(first,last,middle=''):
"""Generate a neatly formatted full name."""
if middle:
full_name = first + ' ' + middle + '' + last
else:
full_name = first + ' ' +last
return full_name.title()
再次运行测试代码,效果如下:
11.1.5 添加新测试
确定get_formatted_name()又能正确地处理简单的姓名后,我们再编写一个测试,用于测试
包含中间名的姓名。为此,我们在NamesTestCase类中再添加一个方法:
import unittest
from name_function import get_formatted_name
class NameTest(unittest.TestCase):
"""测试name_function.py"""
--snip--
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()
11.1动手试一试
11-1 城市和国家:编写一个函数,它接受两个形参:一个城市名和一个国家名。这个函数返回一个格式为 City, Country 的字符串,如 Santiago, Chile。将这个函数存储在一个名city_functions.py 的模块中。
city_functions.py
def City_Country(city,country):
city = input('Please input your city: ')
country = input('Please input your country: ')
msg = city + ", " + country
return msg
创建一个名为 test_cities.py 的程序,对刚编写的函数进行测试(别忘了,你需要导
入模块 unittest 以及要测试的函数)。编写一个名为 test_city_country()的方法,核实使用类似于'santiago'和'chile'这样的值来调用前述函数时,得到的字符串是正确的。运行 test_cities.py,确认测试 test_city_country()通过了。
test_cities.py
import unittest
from city_functions import City_Country
class CityTestCase(unittest.TestCase):
"""测试CityCountry.py"""
def test_city_country(self):
msg = City_Country('Santiago','Chile')
self.assertEqual(msg,'Santiago, Chile')
unittest.main()
11-2 人口数量:修改前面的函数,使其包含第三个必不可少的形参 population,并
返回一个格式为 City, Country – population xxx 的字符串,如 Santiago, Chile –
population 5000000。运行 test_cities.py,确认测试 test_city_country()未通过。
修改上述函数,将形参 population 设置为可选的。再次运行 test_cities.py,确认测
试 test_city_country()又通过了。
再编写一个名为 test_city_country_population()的测试,核实可以使用类似于'santiago'、 'chile'和'population=5000000'这样的值来调用这个函数。再次运行test_cities.py,确认测试 test_city_country_population()通过了。
city_function.py
def City_Country(city, country, population=''):
if population:
msg = city + ", " + country + "-population=" + str(population)
else:
msg = city + ", " + country
return msg
test_cities.py
import unittest
from city_functions import City_Country
class CityTestCase(unittest.TestCase):
"""测试CityCountry.py"""
def test_city_country(self):
msg = City_Country('Santiago', 'Chile')
self.assertEqual(msg, 'Santiago, Chile')
def test_city_country_population(self):
msg = City_Country('Santiago', 'Chile', 5000000)
self.assertEqual(msg, 'Santiago, Chile-population=5000000')
unittest.main()
11.2 测试类
这一部分主要用来学习测试类
11.2.1 各种断言方法
你只能在继承unittest.TestCase的类中使用这些方法。
11.2.2 一个要测试的类
下面来编写一个类进行测试,来看一个帮助管理匿名调查的类:
补充:首先明确的是self只有在类的方法中才会有,独立的函数或方法是不必带有self的。self在定义类的方法时是必须有的,虽然在调用时不必传入相应的参数。
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_result(self):
"""显示收到的所有调查问卷"""
print('Survey result:')
for response in self.responses:
print("-" + response)
language_survey.py
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' 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_result()
定义一个问题,并创建一个AnonyousSurvey对象,显示问题并提示,想要提示就输入'q',进入循环,用户输入第一个学习的语言是什么,添加到response数组里,不断循环,输入'q'退出循环,调用显示结果的函数,将结果打印在屏幕上。
AnonymousSurvey类可用于进行简单的匿名调查。假设我们将它放在了模块survey中,并想进
行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案
出现了多少次;再编写一个类,用于管理非匿名调查。
上述修改行为可能会出现错误,需要编写一个测试类对修改后的类进行测试。
11.2.3 测试 AnonymousSurvey 类
编写一个测试,对AnonymousSurvey类的行为的一个方面进行验证,用户只提供一个答案,这个答案能被妥善处理。将在这个答案被存储后,使用方法assertIn()来核实它包含在答案列表中:
test_survey.py
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)
unittest.main()
要测试类的行为,需要创建其实例。使用问题"What language did you first learn to speak?"创建了一个名为my_survey的实例,然后使用方法store_response()存储了单个答案English。检查English是否包含在列表my_survey.responses中,以核实这个答案是否被妥善地存储了。
下面来核实用户提供三个答案时,它们也将被妥善地存储。
test_survey.py
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_response(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.assertIn(response, my_survey.responses)
unittest.main()
再次运行test_survey.py时,两个测试(针对单个答案的测试和针对三个答案的测试)都通过了。
前述做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。
11.2.4 方法 setUp()
unittest.TestCase类包含方法setUp(),让我们只需创建这些对象一次,并在每个测试方法中使用它们。如果你在TestCase类中包含了方法setUp(), Python将先运行它,再运行各个以test_打头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp()中创建的对象了。
test_survey.py
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_response(self):
"""测试三个答案也能够被妥善存储"""
responses = self.responses
for response in responses:
self.my_survey.store_response(response)
for response in responses:
self.assertIn(response, self.my_survey.responses)
unittest.main()
测试自己编写的类时,方法setUp()让测试方法编写起来更容易:可在setUp()方法中创建一
系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创
建实例并设置其属性,这要容易得多。
注意
运行测试用例时,每完成一个单元测试, Python都打印一个字符:测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F。这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。
11.2动手试一试
11-3 雇员
编写一个名为 Employee 的类,其方法__init__()接受名、姓和年薪,并将它们都存储在属性中。编写一个名为 give_raise()的方法,它默认将年薪增加 5000美元,但也能够接受其他的年薪增加量。为 Employee 编写一个测试用例,其中包含两个测试方法: test_give_default_raise()和 test_give_custom_raise()。使用方法 setUp(),以免在每个测试方法中都创建新的雇员实例。运行这个测试用例,确认两个测试都通过了。
employee.py
class Emoployee():
"""用来显示一个员工的姓名和工资情况"""
def __init__(self, first_name, last_name, salary):
"""初始化员工姓名和工资"""
self.first_name = first_name
self.last_name = last_name
self.salary = salary
def give_raise(self, raise_salary=5000):
"""增加年薪,默认增加5000"""
self.total_salary = raise_salary + self.salary
注意函数中新出现的变量一定要带self.呀,要不然在测试类里面没有被办法调用。
test_employee.py
import unittest
from employee import Emoployee
class TestEmployee(unittest.TestCase):
"""测试employ类"""
def setUp(self):
"""创建类实例"""
self.my_employee = Emoployee('li', 'zeyu', 15000)
def test_give_default_raise(self):
"""默认涨薪"""
self.my_employee.give_raise()
self.assertEqual(self.my_employee.total_salary, 20000)
def test_give_custom_raise(self):
"""定制化涨薪"""
self.my_employee.give_raise(2000)
self.assertEqual(self.my_employee.total_salary, 17000)
unittest.main()
11.3 小结
1.测试函数:导入官方测试类unittest,调用测试函数,给形参赋值,使用assertEqual进行对比
2.测试类,导入官方测试类unittest,实例化类对象,调用对于函数,对于对照列表是否有对于的值assertIn()
3.使用方法setUp()来根据类高效地创建实例并设置其属性,在后面的函数可以调用它