本文正在参加「Python主题月」,详情查看 活动链接
编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。本次将学习如何使用 Python 模块 unittest 中的工具来测试代码,还将学习编写测试用例,核实一系列输入都将得到预期的输出。你将学习如何测试函数和类,并将知道该为项目编写多少个测试。
测试函数
- 要学习测试,必须要有测试代码。以下是一个简单函数,它接受名和姓并返回整洁的姓名:
def get_formatted_name(first, last):
"""生成整洁的姓名。"""
full_name = f"{first} {last}"
return full_name.title()
- 为了核实该函数的作用,编写一个使用该函数的程序。
from name_fucntion import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input(f"Please give me a first name: ")
if first == 'q':
break
last = input(f"Please give me a last name: ")
if last == 'q':
break
formatted_name = get_formatted_name(first, last)
print(f"Neatly formatted name: {formatted_name}.")
- 如果修改了该函数用于保证能够处理中间名,那么修改后就要进行测试。
- 为此,每次修改了该函数都需要运行程序进行测试,十分麻烦。
- 所幸,Python 提供了一种自动测试函数输出的高效方式。
单元测试和测试用例
- Python 标准库中的模块
unittest
提供了代码测试工具。 - 单元测试用于核实函数的某个方面没有问题。
- 测试用例是一组单元测试,它们一道核实函数在各种情形下的行为都符合要求。
可通过的测试
- 要为函数编写测试用例,可先导入模块
unittest
和要测试的函数
,再创建一个继承unittest.TestCase
的类,并编写一系列方法对函数行为的不同方面进行测试。
import unittest
from name_fucntion import get_formatted_name
class NamesTestCase(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')
if __name__ == '__main__':
unittest.main()
- 代码中使用了
unittest
类最有用的功能之一:断言方法。 - 断言方法核实得到的结果是否与期望的结果一致。
- 输出结果如下:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
- 第一行的句点表明有一个测试通过了。接下来的一行指出 Python 运行了一个测试,消耗的时间不到 0.001 秒。最后的 OK 表明该测试用例中的所有单元测试都通过了。
未通过的测试
- 修改
get_formatted_name()
,使其能够处理中间名,但同时故意让该函数无法正确处理像Janis Joplin
这样只有名和姓的姓名。
def get_formatted_name(first, middle, last):
"""生成整洁的姓名。"""
full_name = f"{first} {middle} {last}"
return full_name.title()
- 这个版本应该能够正确处理包含中间名的姓名,但对其进行测试时,我们发现它不再能正确处理只有名和姓的姓名。
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能够正确处理像 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
,指出测试用例中有一个单元测试导致了错误。 - 接下来,我们看到
NamesTestCase
中的test_first_last_name()
导致了错误。
测试未通过时怎么办
- 如果你检查的条件没错,测试通过意味着函数的行为是对的,而测试未通过意味着编写的新代码有错。
- 因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚刚对函数所做的修改,找出导致函数行为不符合预期的修改。
- 就这里而言,最佳的选择是让中间名变为可选的。
def get_formatted_name(first, last, middle=''):
"""生成整洁的姓名。"""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
- 现在,再次运行测试,将得到如下结果:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
添加新测试
- 确定
get_formatted_name()
又能正确处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,在NamesTestCase
类中再添加一个方法:
import unittest
from name_fucntion import get_formatted_name
class NamesTestCase(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_middle_last_name(self):
"""能够正确处理像 Wolfgang Amadeus Mozart这样的姓名吗?"""
fortmatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
self.assertEqual(fortmatted_name, 'Wolfgang Amadeus Mozart')
if __name__ == '__main__':
unittest.main()
- 将该方法命名为
test_first_last_middle_name()
。方法名必须以test_
打头,这样它才会在我们运行test_name_function.py
时自动运行。 - 再次运行
test_name_function.py
时,两个测试都通过了:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
测试类
- 下面来编写针对类的测试。
- 很多程序中都会用到类,因此证明你的类能够正确工作大有裨益。
各种断言方法
- Python 在
unittest.TestCase
类中提供了很多断言方法。 - 断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python 将引发异常。
方法 | 用途 |
---|---|
assertEqual(a, b) | 核实 a == b |
assertNotEqual(a, b) | 核实 a != b |
assertTrue(x) | 核实 x 为 True |
assertFalse(a, b) | 核实 x 为 False |
assertIn(item, list) | 核实 item 在 list 中 |
assertNotIn(item, list) | 核实 item 不在 list 中 |
一个要测试的类
- 类的测试与函数的测试相似,你所做的大部分工作是测试类中方法的行为。
- 先定义一个类:
class AnonymousSurvey:
"""手机匿名调查问卷的答案。"""
def __init__(self, question):
"""存储一个问题,并为存储答案做准备。"""
self.question = question
self.reponses = []
def show_question(self):
"""显示调查问卷。"""
print(self.question)
def store_response(self, new_response):
"""存储单份调查问卷。"""
self.reponses.append(new_response)
def show_result(self):
"""显示收到的所有答卷。"""
print("Survey results:")
for response in self.reponses:
print(f"- {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.")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)
# 显示调查结果。
print("Thank you to everyone who participated in the survey!")
my_survey.show_result()
测试 AnonymousSurvey
类
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)
if __name__ == "__main__":
unittest.main()
- 测试通过:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
- 下面来核实当用户提供三个答案时,它们也将被妥善地存储。
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试。"""
def test_store_single_responses(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_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)
if __name__ == "__main__":
unittest.main()
- 测试通过:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
- 前述做法的效果很好,但这些测试有些重复的地方。下面使用
unittest
的另一项功能来提高其效率。
方法 setUp()
- 在前面的测试中,我们在每个测试方法中都创建了一个
AnonymousSurvey
实例,并在每个方法中都创建了答案。 unittest.TestCase
类包含的方法setUp()
让我们只需创建这些对象一次,就能在每个测试方法中使用。- 如果在
TestCase
类中包含了方法setUp()
,Python 将先运行它,再运行各个以test_
打头的方法。
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']
# return super().setUp()
def test_store_single_responses(self):
"""测试单个答案会被妥善存储。"""
self.my_survey.store_response('English')
self.assertIn('English', 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)
if __name__ == "__main__":
unittest.main()
- 测试通过:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
- 方法
setUp()
做了两件事情:创建一个调查对象,以及创建一个答案列表。 - 存储这两样东西的变量名包含前缀
self
(即存储在属性中),因此可在这个类的任何地方使用。
运行测试用例时,每完成一个单元测试,Python 都打印一个字符:测试通过时打印一个句点
.
,测试引发错误时打印一个E
,而测试导致断言失败时则打印一个F
。