编写函数或类时,可以为其编写测试单元,通过测试,可以确定代码面对各种输入都能按照要求那样工作,在添加新代码时也可以对其进行测试,确保不会破坏既有程序。
首先编写一个程序 name_functions.py,里面包含一个函数 get_formatted_name(),用以处理名和姓,得到全名full_name:
# name_functions.py
def get_formatted_name(first, last):
"""获得一个全名"""
full_name = first + ' ' + last
return full_name.title()
再编写一个程序 name.py:
# name.py
from name_functions import get_formatted_name
print("Enter 'q' at any time to quit")
while True:
first = input('\nFirst name: ')
if first == 'q':
break
last = input('\nLast name: ')
if last == 'q':
break
formatted_name = get_formatted_name(first, last)
print('\Neatly formatted name: ' + formatted_name + '.')
上述程序只能处理名和姓,当有中间名的时候,我们需要修改 get_formatted_name(),使其能处理中间名(middle),又不破坏只有名和姓的方式,为此,我们每次都要在修改 get_formatted_name()后进行测试,这样太繁琐了,索性,python提供了自动测试函数输出的高效方式。
python 标准库中的 unittest 模块提供了代码测试工具:
用于核实函数的某个方面没有问题
是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合预期
良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试,全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式,大型项目要实现全覆盖可能很难,最初一般只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
要为函数创建测试用例,可先导入 unittest 模块以及要测试的函数,再创建一个继承 unittest.TestCase 的类,并编写一系列方法对函数的行为的不同方面进行测试。
下面是一个只包含一个方法的测试用例,它检查函数 get_formatted_name()在给定名和姓时能否正确地工作:
# 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')
(3) self.assertEqual(formatted_name, 'Janis Joplin')
unittest.main()
----------------------------
Ran 1 tests in o.001s # 测试通过
OK
首先,我们导入了模块 unittest 和要测试的函数,接下来我们创建了一个名为NameTestCase 的类(类名随意,最好包含Test),用于包含一系列针对函数的单元测试,这个类继承unittest.TestCase 类,这样python才知道如何运行你编写的测试。
在(3)处,我们使用了 unittest 类最有用的功能之一:一个断言方法,该方法可以用来判断是否与预期一致,有2个参数,第1个为“测试结果”,第2个为“预期结果”,在这里我们是期望 formatted_name 的值为 Janis Joplin。如果相等就好,否则就输出错误信息。
上述程序测试的是只有名和姓的情况,如果我在函数 get_formatted_name()中再指定一个中间名(middle),那么再运行刚才的测试,就会出错:
# name_function.py
def get_formatted_name(first, middle, last):
"""获得全名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
再运行测试程序 test_name_function.py:
# test_name_function.py
Ran 1 test in 0.004s
FAILED (errors=1) # 测试未通过
Error
Traceback (most recent call last):
File "C:\Users\hj\AppData\Local\Programs\Python\Python36-32\lib\unittest\case.py", line 59, in testPartExecutor
yield
File "C:\Users\hj\AppData\Local\Programs\Python\Python36-32\lib\unittest\case.py", line 605, in run
testMethod()
File "C:\Users\hj\PycharmProjects\package\test_get_formatted_name.py", line 7, in test_get_formatted_name
formatted_name = get_formatted_name('tom', 'jerry')
TypeError: get_formatted_name() missing 1 required positional argument: 'middle' # 提示缺少一个实参
测试未通过,都会提示错误信息,根据信息修改原程序,而非测试程序;
在上述示例中,测试未通过提示我们确实一个实参(middle),那么我们修改 函数get_formatted_name(),将middle 设置为可选参数:
# get_formatted_name.py
def get_formatted_name(first, last, middle=''):
"""获得全名"""
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
运行测试程序 test_name_function.py:
# test_name_function.py
Ran 1 test in 0.001s
OK
再编写测试程序,确定其能处理包含中间名的姓名,为此我们在NameTestCase 类中添加个方法:
# test_name_function.py
from unittest import TestCase
from name_function import get_formatted_name
class NameTestCase(TestCase):
def test_first_last_name(self):
"""能处理如Janis Joplin 这样的姓名吗?"""
formatted_name = get_formatted_name('tom', 'jerry')
self.assertEqual(formatted_name, 'Tom Jerry')
def test_middle(self): # 添加新方法用以处理如Tom Li Jerry 这样的名字
"""能处理如Tom Li Jerry 这样的名字?"""
formatted_name = get_formatted_name('tom', 'jerry', 'li')
self.assertEqual(formatted_name, 'Tom Li Jerry')
unittest.main()
-----------------------------
Ran 2 tests in 0.001s # 测试通过,说明程序能够处理.......
OK
踩坑提醒
上述例子通过测试,在 python 自带 IDLE 中,能够正常运行,而使用 pycharm 进行测试函数时,修改原程序函数 get_formatted_name(),再运行测试程序,发现测试结果没有变化,上网查阅了资料得以确定是编辑器的原因,诸如pycharm 类似的编辑器在运行测试程序时,会优先运行本身自带的 unittest 组件,导致修改原函数结果没有变化(绕的我自己都糊涂了),为此在使用 pycharm 测试函数时,我们可以如下操作:(可参考:http://blog.csdn.net/u010816480/article/details/72821535)
测试代码如下:
# test_name_function.py
from unittest import TestCase # 直接导入unittest 模块的TestCase 类,而不是只导入模块
from name_function import get_formatted_name
class NameTestCase(TestCase):
def test_first_last_name(self):
formatted_name = get_formatted_name('tom', 'jerry')
self.assertEqual(formatted_name, 'Tom Jerry')
python 在 unittest.TestCase 类中提供了很多断言方法,使用这些方法可核实返回的值等于或不等于预期的值、返回的值为 True 或 False、返回的值是否在列表中:
方法 | 用途 |
---|---|
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.question)
def store_response(self, new_response):
"""存储单份调查问卷答案"""
self.responses.append(new_response)
def show_results(self):
"""显示调查文件答案"""
print('Survey result: ')
for response in self.responses:
print('-' + response)
为证明 AnonymousSurvey 类能够正确地工作,我们编写一个使用它的程序 language_survey.py:
# language_survey.py
from language_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 survey 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: q
Thank you to survey who participated in the survey!
Survey result:
-english
将AnonymousSurvey 类存放在 模块 survey.py 中,并想进一步改进,让每位用户都可以输入多个答案,编写一个方法,它列出不同答案,并指出每个答案出现了多少次,再编写一个类,用于管理非匿名调查,经过上述修改可能存在风险,可能会影响 AnonymousSurvey 类的当前行为,为此我们可以编写针对这个类的测试。
# test_anonymousSurvey.py
from unittest import TestCase # 导入测试模块的类
from survey import AnonymousSurvey # 导入测试类
class TestAnonymousSurvey(TestCase):
"""针对 AnonymousSurvey 类的测试"""
def test_store_response(self): # 测试单个答案
"""测试单个答案会被妥善地存储"""
question = 'What language did you first learn to speak?'
my_survey = AnonymousSurvey(question) # 创建实例,传入实参 question
my_survey.store_response('English') # 调用store_response方法,将答案存储到属性responses(列表)中
self.assertIn('English', my_survey.responses) # 使用断言方法,判断是否在属性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) # 调用store_response方法,将答案存储到属性responses(列表)中
for responses in responses:
self.assertIn(response, my_survey.responses) # 使用断言方法,判断是否在属性responses中
-------------------------------------
Ran 2 tests in 0.002s # 测试通过
OK
在之前的 test_anonymousSurvey.py 中,每个测试方法,我们都创建了一个 AnonymousSurvey 实例对象,并在每个方法中都创建了一个答案,平白多了几行代码;
而unittest.TestCase 类中包含了方法 setUp(),我们只需在方法 setUp()创建一次实例对象,下面的每个测试方法都将能使用它们,python 也是优先运行它,为此省去了多余的代码:
# test_anonymousSurvey.py
from unittest import TestCase # 导入测试模块的类
from survey import AnonymousSurvey # 导入测试类
class TestAnonymousSurvey(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]) # 调用store_response,并将答案存储到属性responses 中
self.assertIn(self.response[0], self.my_surver.responses) # 断言方法,判断是否在属性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)
-------------------------------
Ran 2 tests in 0.002s
OK
注意
运行测试时,每完成一个单元测试,python 都会打印一个字符:通过打印一个句点,错误打印一个E,测试导致断言失败打印一个F。
在项目中包含初步测试,将使程序更趋完美,在用户报告bug 之前,把问题考虑在内,在项目早期,不要试图去编写全覆盖的测试用例,除非有充分的理由。
练习
编写一个名为 Employee 的类,其方法 init 接受名、姓和年薪,并将他们存储到属性中,编写一个名为 give_raise()的方法,它默认将年薪增加 5000 美元,但也能接受其他的年薪增加量。
为 Employee 编写一个测试用例,包含两个测试方法:test_default_raise()和 test_give_custom_raise(),使用方法 setUp(),以免在每个测试方法都创建新的雇员实例,运行这个测试用例,确认两个测试都通过了。
# name_employee.py
class Employee():
"""编写一个类接受雇员姓名和年薪"""
def __init__(self, first, last, salary ):
"""初始化雇员姓名和年薪"""
self.first_name = first
self.last_name = last
self.salary = salary
def give_raise(self, raise=5000):
"""设置年薪增加量"""
self.salary += raise
# test_employee.py
from unittest import TestCase
from name_employee import Employee
class TestEmployee(TestCase):
"""测试 Employee 类 """
def setUp(self):
"""给 Employee 类创建实例化对象,以供测试使用"""
self.someone = Employee('li', 'la', 65000)
def test_give_default(self):
"""测试默认年薪增加量为 5000 美元"""
self.someone.give_raise()
self.assertEqual(self.someone.salary, 70000)
def test_give_custom_raise(self):
"""测试能接受其他年薪增加量"""
self.someone.give_raise(6000)
self.assertEqual(self.someone.salary, 71000)
-------------------------------------------------------------------------
Ran 2 tests in 0.057s
OK