Python 基础 -- 测试代码
编写函数或者类时,还可以为其编写测试;
通过测试,可以确定代码面对各种输入都能够按照要求的那样工作;
在程序新添加新代码时,可以对其进行测试,确保它们不会破坏程序既有的行为;
使用 Python 模块
unittest
中的测试工具来测试代码;测试代码的最终目的就是核实一系列的输入都将得到预期的输出;
1. 测试函数
-
要学习测试函数,得有测试代码;
-
编写一个简单的函数,它接受名和姓,并返回整洁的姓名;
name_function.py
def get_formatted_name(first, last): """生成完整的名字""" full_name = first + " " + last return full_name.title()
-
为核实
get_formatted_name
像期望的那样工作,编写一个使用这个函数的程序; -
这个程序,输入用户的名和姓,并整洁的显示全名;
names.py
from name_function import get_formatted_name msg = "Enter 'q' at any time to quit!" print(msg) while True: msg = "\nPlease give me a first name: " first = input(msg) if first == 'q': break msg = "Please give me a last name: " last = input(msg) if last == 'q': break formatted_name = get_formatted_name(first, last) msg = "\tNeatly formatted name: " + formatted_name + "." print(msg)
Enter 'q' at any time to quit! Please give me a first name: li Please give me a last name: si Neatly formatted name: Li Si. Please give me a first name: liu Please give me a last name: neng Neatly formatted name: Liu Neng. Please give me a first name: zhao Please give me a last name: si Neatly formatted name: Zhao Si. Please give me a first name: q ------------------ (program exited with code: 0) Press return to continue
-
从上述输出可知,合并得到的姓名正确无误;
-
但是当修改后,每次对函数进行测试就太繁琐了;
-
所幸 Python 提供了一种自动测试函数的高效方式;
1.1 单元测试和测试用例
- Python 标准库中的模块
unittest
提供了代码测试工具; - 单元测试用于核实函数的某个方面没有问题;
- 测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求;
- 良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试;
- 全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式;
- 对于大型项目,要实现全覆盖可能很难;
- 通常,最初只要针对代码的重要性为编写测试即可,等项目被广泛使用是再考虑全覆盖;
1.2 可通过的测试
-
要为函数编写测试用例;
-
可先导入模块
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): formatted_name = get_formatted_name('li', 'si') self.assertEqual(formatted_name, 'Li Si') unittest.main()
- 第 1 行,导入了 Python 提供的标准库中的模块
unittest
; - 第 2 行,导入了要测试的函数
get_formatted_name
; - 第 4 行,创建了一个名为
NameTestCase
的类,用于包含一系列针对get_formatted_name()
的单元测试;- 这个类的命名建议:
- 看起来与测试函数相关;
- 包含 Test 字样;
- 这个类必须继承
unittest.TestCase
;
- 这个类的命名建议:
- 第 7 行,定义一个用于测试
get_formatted_name()
的一个方法;- 这个方法命名为
test_first_last_name()
; - Python 在执行测试用例时,会自动执行所有以
test
开头的方法,所以测试用例里边的方法都需要以test
开头命名;
- 这个方法命名为
- 第 8 行,调用了要测试的函数,并存储了要测试的返回值;
- 第 9 行,使用了
unittest
类最有用的功能之一:断言方法;- 断言方法:核实得到的结果是否与期望的结果一致;
- 第 11 行,让 Python 运行这个文件中的测试;
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ------------------ (program exited with code: 0) Press return to continue
- 第 1 行,
.
表示通过; - 第 3 行,表示消耗的时间;
- 第 5 行,表示该测试用例中的所有单元测试都通过了;
- 第 1 行,导入了 Python 提供的标准库中的模块
1.3 不能通过的测试
-
我们通过修改
get_formatted_name()
函数,来使得测试不通过,看看结果;name_function.py
def get_formatted_name(first, middle, last): """生成完整的名字""" full_name = first + " " + middle + " "+ last return full_name.title()
-
这个版本包含了中间名,对其进行测试:
E ====================================================================== ERROR: test_first_last_name (__main__.NameTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_name_function.py", line 8, in test_first_last_name formatted_name = get_formatted_name('li', 'si') TypeError: get_formatted_name() missing 1 required positional argument: 'last' ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (errors=1) ------------------ (program exited with code: 1) Press return to continue
- 第 1 行,
E
表示测试用例中有一个单元测试导致了错误; - 第 3 行,显示了错误的函数,当单元测试比较多时,知道那个测试未通过至关重要;
- 第 5 行,看到一个标准的
traceback
,它指出函数调用get_formatted_name('li', 'si')
有问题; - 第 8 行,指出问题是缺少一个必要的位置实参;
- 第 11 行,表示运行了一个单元测试,消耗的时间;
- 第 13 行,告诉你测试用例没通过;
- 第 1 行,
1.4 测试未通过时怎么办
-
检查条件如果没有出错,测试通过了意味着函数的行为是对的,而测试没通过意味着编写的新代码有错;
-
测试未通过时,不要修改测试,而应该修复导致测试不能通过的代码;
-
检查对函数所作的修改,找出导致函数行为不符合预期的修改;
-
还有一种可能是,修改了函数以及测试用例后没有再次保存函数;
-
针对上述代码,我们可以将中间名设置为可选项:
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()
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ------------------ (program exited with code: 0) Press return to continue
1.5 添加新测试
-
确定
get_formatted_name()
能够正确处理简单姓名之后,我们在编写一个测试,用于测试包含中间名的姓名; -
为此,我们在
NameTestCase
类中添加一个新的方法;import unittest from name_function import get_formatted_name class NameTestCase(unittest.TestCase): """测试 name_function.py""" def test_first_last_name(self): formatted_name = get_formatted_name('li', 'si') self.assertEqual(formatted_name, 'Li Si') def test_first_middle_last_name(self): formatted_name = get_formatted_name('li', 'si', 'liu') self.assertEqual(formatted_name, 'Li Liu Si') unittest.main()
- 方法命名必须以
test
开头; - 方法名可以是很长的名字;
- 方法名必须是描述性的;
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK ------------------ (program exited with code: 0) Press return to continue
- 方法命名必须以
1.6 练习
1. 城市和国家
-
编写一个函数,它接收两个形参:一个城市名和一个国家名;
-
这个函数返回一个格式为 City, Country 的字符串,如 Santiago, Chile;
-
将这个函数存储在一个名为 city_functions.py 的模块中;
-
创建一个名为 test_cities.py 的程序,对刚编写的函数进行测试;
-
编写一个名为 test_city_country() 的方法,核实使用类似于
santiago
和chile
这样的值来调用前述函数时,得到的字符串是正确的; -
运行 test_cities.py,确认测试 test_city_country() 通过了;
city_country.py
def city_country_soon(city, country): msg = city.title() + ", " + country.title() return msg
test_cities.py
import unittest from city_country import city_country_soon class CityTestCase(unittest.TestCase): """测试 city_country.py""" def test_city_country(self): msg = city_country_soon('santiago', 'chile') self.assertEqual(msg, 'Santiago, Chile') unittest.main()
. ---------------------------------------------------------------------- Ran 1 test in 0.002s OK ------------------ (program exited with code: 0) Press return to continue
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_populaiton() 的测试,核实可以使用类似于
santiago
、chile
和population=5000000
这样的值来调用这个函数; -
再次运行 test_cities.py 确认 test_city_country_populaiton() 通过了;
city_country.py
def city_country_soon(city, country, population=''): if population: msg = city.title() + ", " + country.title() + "-population" + " " + str(population) else: msg = city.title() + ", " + country.title() return msg
test_cities.py
import unittest from city_country import city_country_soon class CityTestCase(unittest.TestCase): """测试 city_country.py""" def test_city_country(self): msg = city_country_soon('santiago', 'chile') self.assertEqual(msg, 'Santiago, Chile') def test_city_country_populaiton(self): msg = city_country_soon('santiago', 'chile', 5000000) self.assertEqual(msg, 'Santiago, Chile-population 5000000') unittest.main()
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK ------------------ (program exited with code: 0) Press return to continue
2. 测试类
- 对类进行测试也有很大的裨益;
- 若针对类的测试通过了,就可以确信对类所做的改进没有意外的破坏其原有的行为;
2.1 断言方法
-
之前在测试函数中了解过一种断言方法;
-
所谓的断言方法就是判断你所认为应该满足的条件是否被满足;
-
常用的断言方法;
方法 用途 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 中
2.2 一个要测试的类
-
类的测试与函数的测试相似,大部分工作都是测试类中方法的行为;
-
编写一个帮住管理匿名调查的类;
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): """显示收集到的所有答卷""" msg = "Survey results: " print(msg) for response in self.responses: msg = "- " + response print(msg)
- 创建这个类的实例,只需要提供一个问题即可;
- 有了表示调查的实例后,就可以使用
show_question()
来显示其中的问题; - 使用
store_response()
来存储答案; - 使用
show_results()
来显示调查结果;
-
为了证明
AnonymousSurvey
类可以正确的工作,来编写一个使用它的程序: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() msg = "Enter 'q' at any time to quit.\n" print(msg) while True: msg = "language: " response = input(msg) if response == 'q': break my_survey.store_response(response) # 显示调查结果 msg = "\nThank you to every who participated in the survey!" print(msg) my_survey.show_results()
What language did you first learn to speak? Enter 'q' at any time to quit. language: chinese language: C language: English language: Jav language: q Thank you to every who participated in the survey! Survey results: - chinese - C - English - Jav ------------------ (program exited with code: 0) Press return to continue
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()
- 测试类与测试函数的操作基本相同,唯一不同就是需要创建类的实例,如第 10 行;
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ------------------ (program exited with code: 0) Press return to continue
-
测试用户提供三个答案也能被妥善保存;
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_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) unittest.main()
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK ------------------ (program exited with code: 0) Press return to continue
2.4 setUp() 方法
-
之前的测试函数效果很好,但是有重复的地方,略显得累赘;
-
使用
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_response(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) for response in self.responses: self.assertIn(response, self.my_survey.responses) unittest.main()
- 第 7 行,
setUp()
方法做了两件事:- 创建对象,第 10 行;
- 创建一个列表答案,第 11 行;
- 因为存储这两样变量的前缀都带有
self
,所以可以在这个类的任意地方使用;
- 第 7 行,
2.5 练习 – 雇员
-
编写一个名为
Employee
的类,其方法__init__()
接受名、姓和年薪,并将它们都存储在属性中; -
编写一个名为
give_raise()
的方法,它默认将年薪增加 5000 美元,但也能够接受其他的年薪增加量; -
为
Employee
编写一个测试用例,其中包含两个测试方法: -
test_give_default_raise()
和test_give_custom_raise()
; -
使用方法
setUp()
,以免在每个测试方法中都创建新的雇员实例; -
运行这个测试用例,确认两个测试都通过了;
class Employee(): """这是一个雇员类""" def __init__(self, first, last, salary): """雇员的基本信息""" self.first_name = first self.first_last = last self.salary = salary def give_raise(self, add=5000): """雇员的年薪增加""" self.salary += add
import unittest from employee_3 import Employee class TestEmployee(unittest.TestCase): def setUp(self): """创建一组雇员信息,提供用的测试方法""" self.employee = Employee('li', 'si', 5000) def test_give_default_raise(self): """测试默认加工资""" self.employee.give_raise() self.assertEqual(self.employee.salary, 10000) def test_give_custom_raise(self): """测试自定义的加工资""" self.employee.give_raise(10000) self.assertEqual(self.employee.salary, 15000) unittest.main()
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK ------------------ (program exited with code: 0) Press return to continue
- 进行测试用例时,每完成一个单元测试,Python 都打印一个字符;
- 测试通过时打印一个
.
;- 测试引发错误时打印一个
E
;- 测试导致断言失败时打印一个
F
;