【早起挑战第4天】7点,算是成功一天吧,珍惜时间。
第11章 测试代码
编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。测试让你信心满满,深信即便有更多的人使用你的程序,它也能正确地工作。在程序中添加新代码时,你也可以对其进行测试,确认它们不会破坏程序既有的行为。程序员都会犯错,因此每个程序员都必须经常测试其代码,在用户发现问题前找出它们。
在本章中,你将学习如何使用Python模块unittest 中的工具来测试代码。你将学习编写测试用例,核实一系列输入都将得到预期的输出。你将看到测试通过了是什么样子,测试未通过又是什么样子,还将知道测试未通过如何有助于改进代码。你将学习如何测试函数和类,并将知道该为项目编写多少个测试。
11.1 测试函数
要学习测试,得有要测试的代码。下面是一个简单的函数,它接受名和姓并返回整洁的姓名:
def get_formatted_name(first,last):
full_name = first + ' ' + last
return full_name.title()
函数get_formatted_name() 将名和姓合并成姓名,在名和姓之间加上一个空格,并将它们的首字母都大写,再返回结果。为核实get_formatted_name() 像期望的那样工作,我们来编写一个使用这个函数的程序。程序names.py让用户输入名和姓,并显示整洁的全名:
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 + '.')
运行结果:
从上述输出可知,合并得到的姓名正确无误。现在假设我们要修改get_formatted_name() ,使其还能够处理中间名。这样做时,我们要确保不破坏这个函数处理只有名和姓的姓名的方式。为此,我们可以在每次修改get_formatted_name() 后都进行测试:运行程序names.py,并输入像Janis Joplin 这样的姓名,但这太烦琐了。所幸Python提供了一种自动测试函数输出的高效方式。倘若我们能对get_formatted_name() 进行自动测试,就能始终信心满满,确信给这个函数提供我们测试过的姓名时,它都能正确地工作。
11.1.1 单元测试和测试用例
Python标准库中的模块unittest 提供了代码测试工具。单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
11.1.2 可通过的测试
创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest 以及要测试的函数,再创建一个继承unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。
下面是一个只包含一个方法的测试用例,它检查函数get_formatted_name() 在给定名和姓时能否正确地工作:
import unittest
from name_function import get_formatted_name
#测试name_function.py
class NameTestCase(unittest.TestCase):
def test_first_last_name(self):
formatted_name = get_formatted_name('janis','joplin')
self.assertEqual(formatted_name,'Janis Joplin')
unittest.main()
运行结果:
上述输出表明,给定包含名和姓的姓名时,函数get_formatted_name() 总是能正确地处理。
11.1.3 不能通过的测试
测试未通过时结果是什么样的呢?我们来修改get_formatted_name() ,使其能够处理中间名,但这样做时,故意让这个函数无法正确地处理像Janis Joplin这样只有名和姓的姓名。
下面是函数get_formatted_name() 的新版本,它要求通过一个实参指定中间名:
def get_formatted_name(first,middle,last):
full_name = first + ' ' +middle + ' ' + last
return full_name.title()
运行结果:
11.1.4 测试未通过时怎么办
测试未通过时怎么办呢?如果你检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
在这个示例中,get_formatted_name() 以前只需要两个实参——名和姓,但现在它要求提供名、中间名和姓。新增的中间名参数是必不可少的,这导致get_formatted_name() 的行为不符合预期。就这里而言,最佳的选择是让中间名变为可选的。这样做后,使用类似于Janis Joplin的姓名进行测试时,测试就会通过了,同时这个函数还能接受中间名。下面来修改get_formatted_name() ,将中间名设置为可选的,然后再次运行这个测试用例。如果通过了,我们接着确认这个函数能够妥善地处理中间名。
要将中间名设置为可选的,可在函数定义中将形参middle 移到形参列表末尾,并将其默认值指定为一个空字符串。我们还要添加一个if 测试,以便根据是否提供了中间名相应地创建姓名:
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 类中再添加一个方法:
import unittest
from name_function import get_formatted_name
#测试name_function.py
class NameTestCase(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()
运行结果:
太好了!现在我们知道,这个函数又能正确地处理像Janis Joplin这样的姓名了,我们还深信它也能够正确地处理像WolfgangAmadeus Mozart这样的姓名。
11.2 测试类
在本章前半部分,你编写了针对单个函数的测试,下面来编写针对类的测试。很多程序中都会用到类,因此能够证明你的类能够正确地工作会大有裨益。如果针对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的行为。
11.2.1 各种断言方法
Python在unittest.TestCase 类中提供了很多断言方法。前面说过,断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,你就可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python将引发异常。
表11-1描述了6个常用的断言方法。使用这些方法可核实返回的值等于或不等于预期的值、返回的值为True 或False 、返回的值在列表中或不在列表中。你只能在继承unittest.TestCase 的类中使用这些方法,下面来看看如何在测试类时使用其中的一个。
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 一个要测试的类
类的测试与函数的测试相似——你所做的大部分工作都是测试类中方法的行为,但存在一些不同之处,下面来编写一个类进行测试。来看一个帮助管理匿名调查的类:
#收集匿名调查问卷的答案
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)
为证明AnonymousSurvey 类能够正确地工作,我们来编写一个使用它的程序:
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_results()
运行结果:
AnonymousSurvey 类可用于进行简单的匿名调查。假设我们将它放在了模块survey 中,并想进行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。
进行上述修改存在风险,可能会影响AnonymousSurvey 类的当前行为。例如,允许每位用户输入多个答案时,可能不小心修改了处理单个答案的方式。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。
11.2.3 测试AnonymousSurvey类
下面来编写一个测试,对AnonymousSurvey 类的行为的一个方面进行验证:如果用户面对调查问题时只提供了一个答案,这个答案也能被妥善地存储。为此,我们将在这个答案被存储后,使用方法assertIn() 来核实它包含在答案列表中:
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()
运行结果
这很好,但只能收集一个答案的调查用途不大。下面来核实用户提供三个答案时,它们也将被妥善地存储。为此,我们在TestAnonymousSurvey 中再添加一个方法:
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)
#测试三个答案会被妥善地存储
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()
运行结果:
11.2.4 方法setUp()
在前面的test_survey.py中,我们在每个测试方法中都创建了一个AnonymousSurvey 实例,并在每个方法中都创建了答案。unittest.TestCase 类包含方法setUp() ,让我
们只需创建这些对象一次,并在每个测试方法中使用它们。如果你在TestCase 类中包含了方法setUp() ,Python将先运行它,再运行各个以test_打头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp() 中创建的对象了。
下面使用setUp() 来创建一个调查对象和一组答案,供方法test_store_single_response() 和test_store_three_responses() 使用:
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', 'Mandarin']
#测试单个答案会被妥善地存储
def test_store_single_response(self):
self.my_survey.store_response(self.responses[0])
self.assertIn('English',self.my_survey.responses)
#测试三个答案会被妥善地存储
def test_store_three_response(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() 让测试方法编写起来更容易:可在setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。
11.3 小结
在本章中,你学习了:
- 如何使用模块unittest 中的工具来为函数和类编写测试
- 如何编写继承unittest.TestCase 的类
- 如何编写测试方法,以核实函数和类的行为符合预期
- 如何使用方法setUp() 来根据类高效地创建实例并设置其属性,以便在类的所有测试方法中都可使用它们
明天要回顾一下练习题,巩固一下基础部分所学知识~就到这里吧。