学习阶梯
《Python编程:从入门到实践》
第11章 测试代码
编写函数或类时,还可为其编写测试。通感测试,可确定代码,面对各种输入都能按照要求的那样工作。在程序中添加新代码时,也可以对其进行测试,确认它们不会破坏程序既有的行为。在python中测试代码可以使用模块unitest中的工具进行。
- 测试函数
要测试的代码
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("\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+'.')
要确保修改后程序运行正确,每次都需要运行并输入名字,太过繁琐,python提供了一种自动测试函数输出的高效方式。
- 单元测试和测试用例
Python标准库中的模块unittest提供了代码测试工具。
单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能受到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
- 可通过的测试
要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。
test_name_function.py
import unittest
from name_function import get_formatted_name
class NameTestCase(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_print(self):
print("测试是否所有以test_打头的方法都将自动运行")
unittest.main()
名为NamesTestCase的类,用于包含一些系列针对get_formatted_name()的单元测试。类的命名包含字样Test而且看起来跟要测试的函数相关会更好。这个类必须继承unittest.TestCase类,这样python才知道如何运行编写的测试。
运行test_name_function.py时,所有以test_打头的方法都将自动运行。在方法中调用了要测试的函数,并存储了要测试的返回值。
**思考:**简单测试了一下,真的所有以test_打头的方法都自动运行,猜测也许是unittest这个模块编写时做了处理,明确只对特殊字符开头的函数进行测试运行。
**断言方法:**用来合适得到的结果是否与期望的结果一致。调用unittest的方法assertEqual(),将formatted_name的值同字符串’janis Joplin’进行比较,如果相等,就通过;不相等,就说明。
运行结果如下:
.测试是否所有以test_打头的方法都将自动运行
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
第1、2行的句点表明有一个测试通过了。
第4行表示在多少时间内运行了多少个测试用例
最后的"OK"表明该测试用例中的所有单元测试都通过了。
- 不能通过的测试
name_function.py
def get_formatted_name(first,middle,last):
"""生成整洁的姓名"""
full_name=first+' '+middle+' '+last
return full_name.title()
运行test_name_function.py,输出如下:
E
======================================================================
ERROR: test_first_last_name (__main__.NameTestCase)
能够正确地处理像Janis Joplin这样的姓名吗?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_name_function.py", line 9, 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)
第一行输出只有一个字母E,指出测试用例中有一个单元测试导致了错误。测试用例包含众多单元测试时,知道哪个测试未通过至关重要。最后,看到的一条消息,它指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误。
- 测试未通过时怎么办
如果检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着编写的新代码有错。
测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
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()
要将中间名设置为可选的,可在函数定义中将形参middle移到形参列表末尾,并将其默认值指定为一个空字符串。
无需手工测试这函数,函数修复后测试用例通过,因为未通过的测试可以得知新代码破坏了函数原来的行为。
- 添加新测试
test_name_function.py
import unittest
from name_function import get_formatted_name
class NameTestCase(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_打头,这样它才会在我们运行test_name_function.py时自动运行。在TestCase类中使用很长的方法名是可以的;这些方法的名称必须是描述性的,这才能让你明白测试未通过时的输出;这些方法由Python自动调用,你根本不用编写调用它们的代码。
练习
11-1 城市和国家:编写一个函数,它接受两个形参:一个城市名和一个国家名。这个函数返回一个格式为 City, Country 的字符串,如 Santiago, Chile。将这个函数存储在一个名为 city _functions.py 的模块中。
创建一个名为 test_cities.py 的程序,对刚编写的函数进行测试(别忘了,你需要导入模块 unittest 以及要测试的函数)。编写一个名为 test_city_country()的方法,核实使用类似于’santiago’和’chile’这样的值来调用前述函数时,得到的字符串是正确的。运行 test_cities.py,确认测试 test_city_country()通过了。
"""city_functions.py"""
def get_city_country(city, country):
city_country =city.title()+" is in "+country.title()+"."
return city_country
"""test_cities.py"""
import unittest
from city_functions import get_city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_country = get_city_country('santiago','chile')
self.assertEqual(formatted_country,'Santiago is in 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_functions.py"""
def get_city_country(city, country,population=''):
if population:
city_country =city.title()+" , "+country.title()+"-"+str(population)+"."
else:
city_country =city.title()+" is in "+country.title()+"."
return city_country
"""test_cities.py"""
import unittest
from city_functions import get_city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_country = get_city_country('santiago','chile')
self.assertEqual(formatted_country,'Santiago is in Chile.')
def test_city_country_population(self):
formatted_country = get_city_country('santiago','chile',5000000)
self.assertEqual(formatted_country,'Santiago , Chile-5000000.')
unittest.main()
- 测试类
很多程序中都会用到类,因此能够证明类能正确地工作会大有裨益。如果针对类的测试通过了你就能确认对类所做的改进没有意外地破坏其原有的行为。
- 各种断言方法
断言方法检查认为应该满足的条件是否确实满足。如果该条件确实满足,对程序行为的假设就得到了确认,可以确认其中没有错误;如果认为应该满足的条件实际上并不满足,Python将引发异常。
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中 |
- 一个要测试的类
类的测试与函数的测试相似–所做的大部分工作都是测试类中方法的行为,但是存在以些不同之处。
survey.py
class AnonymousSurvey():
"""收集匿名调查问卷的答案"""
def __init__(self,question):
"""存储一个问题,并为存储答案做准备"""
self.question = question
self.responses=[]
def show_question(self):
"""显示调查问卷"""
print(self)
def store_response(self, new_response):
"""存储单份调查答卷"""
self.responses.append(new_response)
def show_results(self):
"""显示收集到的所有答案"""
print("Survey results:")
for response in 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_results()
AnonymousSurvey类可用于进行简单的匿名调查。假设将它放在了模块survey中,并进行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现多少次;再编写一个类,用于管理非匿名调查。
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.title())
def show_results(self):
"""显示收集到的所有答案"""
print("Survey results:")
for response in self.responses:
print('- '+response)
def results_times(self):
"""显示每个答案出现的次数"""
formatLists=list(set(self.responses))
formatLists.sort(key=self.responses.index)
''' print("去除重复答案后的列表:"+str(formatLists)) '''
for formatList in formatLists:
count=0
for response in self.responses:
if formatList ==response:
count+=1
print("The "+formatList+" appears "+str(count)+" times.")
class NonAnonymousSurvey(AnonymousSurvey):
"""收集非匿名调查问卷的答案"""
def __init__(self,question):
super().__init__(question)
def show_results(self,username):
"""显示收集到的所有用户名和对应答案"""
print("Survey results:")
for response in self.responses:
print("username-response: "+username.title()+" - "+response)
language_survey.py
from survey import AnonymousSurvey,NonAnonymousSurvey
question="What language did you first learn to speak?"
while True:
print("\nDo you need to be anonymous?")
print("(Enter 'q' at any time to quit.)")
choice=input("Please enter 'Yes' or 'No'")
if choice=='q':
break
else:
if choice.title()=='Yes':
my_survey=AnonymousSurvey(question)
my_survey.show_question()
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()
my_survey.results_times()
elif choice.title() =='No':
sample_survey=NonAnonymousSurvey(question)
username=input("\nPlease enter your username: ")
if username=='q':
break
sample_survey.show_question()
while True:
response=input("language: ")
if response=='q':
break
sample_survey.store_response(response)
print("\nThank you to everyone who participated in the survey!")
sample_survey.show_results(username)
sample_survey.results_times()
else:
print("Choice input error!")
进行上述修改存在风险,可能会影响AnonymousSurvey类的当前行为。例如,允许每位用户输入多个答案时,可能不小心修改了处理单个答案的方式。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。
test_language_survey.py
import unittest
from survey import AnonymousSurvey
class SurveyTest(unittest.TestCase):
def test_response(self):
"""测试能否输入多个回答"""
question="Test question?"
test_survey=AnonymousSurvey(question)
print("(Enter 'q' at any time to quit.)")
while True:
response=input("Enter survey response:")
if response =='q':
break
test_survey.store_response(response)
self.assertIn(response.title(),test_survey.responses)
unittest.main()
- 测试AnonymousSurvey类
对AnonymousSurvey类的行为的一个方面进行验证:如果用户面对调查问题时只提供了一个答案,这个答案也能被妥善地存储。为此,我们将在这个答案被存储后,使用方法assertIn()来核实它包含在答案列表中。
哇的一声哭出来,没看下面的内容,自己在那捣鼓了好久,写好了,回头一看,才发现这部分内容有呈现,在下面…(ಥ_ಥ)
test_survey.py
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""
def test_store_single_survey(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()
但只能收集一个答案的调查用途不大。下面来核实用户提供三个答案时,它们也将被妥善地存储
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.assertIn(response, my_survey.responses)
我把整体代码进行了整理:
import unittest
from survey import AnonymousSurvey
question="What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""
def test_store_single_survey(self):
"""测试单个答案会被妥善地的存储"""
my_survey.store_response('English')
self.assertIn('English',my_survey.responses)
def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
responses=['English','Spanish','Mandarin']
for response in responses:
my_survey.store_response(response)
self.assertIn(response, my_survey.responses)
unittest.main()
这里question和my_survey最初是放在类里的,但是运行会报错,只能把这部分提出到类外,或者在每个函数写一遍,这样清晰但是会比较繁琐,我不喜欢,所以还是把这部分提出到类外了,代码编写方式有多种,可以根据个人风格进行编写,最后达到的目的和结果都是一样的。
前述做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。
想哭,又是之后才看到上面这句话,自己还傻傻的整理代码…〒▽〒
- 方法setUp()
unittest.TestCase类包含方法setUp(),让我们只需创建这些对象一次,并在每个测试方法中使用它们。如果TestCase类中包含了方法setUp(),Python将先运行它,再运行各个以test_打头的方法。这样,在编写的每个测试方法中都可使用在方法setUp()中创建的对象了。
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_survey(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)
self.assertIn(response,self.my_survey.responses)
unittest.main()
方法setUp()做了两件事情:创建一个调查对象;创建一个答案列表。存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用。
测试自己编写的类时,方法setUp()让测试方法编写起来更容易:可在setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。
**注:**运行测试用例时,每完成一个单元测试,Python都打印一个字符:测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。
练习
11-3 雇员:编写一个名为 Employee 的类,其方法__init__()接受名、姓和年薪,并将它们都存储在属性中。编写一个名为 give_raise()的方法,它默认将年薪增加 5000美元,但也能够接受其他的年薪增加量。
employee.py
class Employee():
"""员工类"""
def __init__(self,first_name,last_name,annual_salary):
"""基本信息"""
self.first_name = first_name
self.last_name = last_name
self.annual_salary = annual_salary
def get_fullname(self):
"""得到完整的名字"""
full_name=self.first_name.title()+' '+self.last_name.title()
print(full_name)
return full_name
def give_raise(self,increment=5000):
"""年薪增加量"""
annual_salary=self.annual_salary+increment
print("The annual salary after the increase is "+str(annual_salary)+".")
return annual_salary
为 Employee 编写一个测试用例,其中包含两个测试方法:test_give_default_ raise()和 test_give_custom_raise()。使用方法 setUp(),以免在每个测试方法中都创建新的雇员实例。运行这个测试用例,确认两个测试都通过了。
test_employee.py
import unittest
from employee import Employee
class TestEmployee(unittest.TestCase):
"""针对Employee类的测试"""
def setUp(self):
"""创建一个员工和一组信息,供使用的测试方法使用"""
self.test_employee=Employee('john','smith',60000)
def test_get_fullname(self):
"""测试得到完整姓名的函数是否正确"""
fullname = self.test_employee.get_fullname()
self.assertEqual(fullname, 'John Smith')
def test_give_raise(self):
"""测试年薪增加量函数是否正确"""
annually_salary1=self.test_employee.give_raise(8000)
self.assertEqual(annually_salary1, 68000)
annually_salary2=self.test_employee.give_raise()
self.assertEqual(annually_salary2, 65000)
unittest.main()
注:第一部分–基础知识,结束啦!!!