Python 单元测试(unittest)实战指南:从函数到类的完整测试流程
本文详解Python标准库unittest的使用,涵盖测试目录结构规范、函数与类的测试策略。通过get_city_info和CourseManage示例,演示如何编写测试用例、处理断言失败及升级函数兼容性,并深入解析assertEqual等常用断言方法及setUp()优化技巧,帮助开发者建立高效的自动化测试体系。
测试目录
关于项目的整体目录结构,可参考通用的“软件目录开发规范”,此处重点讨论测试目录的构建。通常做法是在项目根目录下单独创建一个名为“tests”的文件夹来存放所有测试文件。
关于测试目录的具体位置,一种规范建议是在项目名(假设为Foo)的一级子目录下创建二级子目录“Foo/foo/tests”。然而,考虑到实际使用的便利性,许多开发者选择将测试目录提升为一级子目录,直接置于项目根目录下,即“Foo/tests”。参考Django、Scrapy和Flask等主流框架,均采用这种将tests作为一级子目录的做法。尽管存在上述更便捷的方式,本文示例仍沿用“Foo/foo/tests”的结构进行演示。
测试函数
此处“测试函数”特指对普通函数(def)进行测试,以区别于对类(class)的测试。
进行单元测试前,需要有一段待测代码。以下是一个简单的示例函数,它接收城市名和国家名作为参数,并返回格式为“City, Country”的字符串:
# UnitTest/unit_test/utils/city_functions.py def get_city_info(city, country): city_info = "%s, %s" % (city, country) return city_info.title()
接下来,我们将对上述函数编写相应的测试代码。
手动测试
首先,编写一个使用该函数的交互程序:
# UnitTest/unit_test/test/cities.py
try:
from unit_test.utils.city_functions import get_city_info
except ModuleNotFoundError:
import sys
sys.path.append('../..')
from unit_test.utils.city_functions import get_city_info
print("Enter 'q' at any time to quit.")
while True:
city = input("city name: ")
if city == 'q':
break
country = input("country name: ")
if country == 'q':
break
fullname = get_city_info(city, country)
print("\tcity info:", fullname)
运行该程序后,得到的输出结果如下:
Enter 'q' at any time to quit. city name: shanghai country name: china city info: Shanghai, China city name: q
Process finished with exit code 0
上述过程属于手动测试,效率较低。为了实现高效自动化,我们需要一种自动测试函数输出的机制。通过自动测试get_fullname()(注:原文此处函数名与上下文get_city_info可能不一致,按原文保留),可以确保在提供经过验证的输入时,函数始终返回正确结果,尤其是在对函数进行修改前后,这能极大提升代码修改的信心。
模块导入路径的问题
在PyCharm中,IDE会自动将项目目录添加到环境变量中,因此直接运行通常没有问题。但如果脱离IDE单独运行脚本,由于目录结构的原因,可能会找不到待测试的函数。解决这一问题最简单的方法是将测试用例文件与被测试函数放置在同一个目录中,并相应调整from import语句即可正常运行。或者,也可以像示例代码那样,手动将路径添加到环境变量中。
单元测试-unittest
Python标准库提供了unittest模块,专门用于代码测试。
创建测试用例
为函数编写测试用例时,首先需导入unittest模块及待测函数,然后创建一个继承自unittest.TestCase的类,并在其中编写一系列方法,以从不同方面测试函数的行为。
以下是一个仅包含一个测试方法的测试用例示例:
# UnitTest/unit_test/test/test_city_functions.py
import unittest
try:
from unit_test.utils.city_functions import get_city_info
except ModuleNotFoundError:
import sys
sys.path.append('../..')
from unit_test.utils.city_functions import get_city_info
class CitiesTestCase(unittest.TestCase):
"""测试city_functions.py"""
def test_city_country(self):
city_info = get_city_info('shanghai', 'china')
self.assertEqual(city_info, 'Shanghai, China')
def test_New_York(self):
city_info = get_city_info('new york', 'America')
self.assertEqual(city_info, 'New York, America')
if __name__ == '__main__':
unittest.main()
命名规则与建议:
- 测试类的名称可以任意指定,但建议名称与测试内容相关,并包含“Test”字样。
- 测试方法的名称必须以“test_”开头。所有以“test_”开头的方法在运行测试时都会被自动执行。
在测试方法的末尾,使用了unittest类中极具价值的功能之一:断言方法。通过断言,我们可以检查实际得到的结果是否与预期结果一致。
输出的效果
代码最后一行的unittest.main()用于运行该文件中的测试。执行测试程序后,得到的输出如下:
. ---------------------------------------------------------------------- Ran 1 test in 0.000s
OK
在运行测试用例时,每完成一个单元测试,Python会打印一个字符:
- 当测试通过时,打印一个句点(.);
- 当测试引发错误时,打印一个E;
- 当测试导致断言失败时,打印一个F。
这就是在输出第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含大量单元测试,运行时间较长,可以通过观察这些字符来快速了解有多少测试已通过。
PyCharm对单元测试进行了优化,输出界面更加美观,不再直接显示上述的点,而是提供更友好的可视化展示。
测试不通过
接下来观察测试不通过的情况。此处不修改测试用例,而是对get_city_info()函数进行更新,要求现在还需显示城市的人口数量:
def get_city_info(city, country, population): city_info = "%s, %s - 人口: %d" % (city, country, population) return city_info.title()
再次执行测试用例,输出如下:
E ====================================================================== ERROR: test_city_country (__main__.CitiesTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_city_functions.py", line 17, in test_city_country city_info = get_city_info('shanghai', 'china') TypeError: get_city_info() missing 1 required positional argument: 'population'
---------------------------------------------------------------------- Ran 1 test in 0.001s
FAILED (errors=1)
此处观察到的是E而非之前的点,表示有一个测试错误。
测试未通过的处理
此时不应修改之前的测试用例。假设更新前的函数已在项目中使用,测试不通过意味着之前调用该函数的代码可能存在问题。如果不想修改项目中其他代码,可以尝试修改get_city_info()函数以通过测试,同时添加新功能:
# UnitTest/unit_test/utils/city_functions.py
def get_city_info(city, country, population=None):
if population:
city_info = "%s, %s - 人口: %d" % (city, country, population)
else:
city_info = "%s, %s" % (city, country)
return city_info.title()
现在的各个版本的update实现了向后兼容,此次测试用例即可通过。
添加新测试
之前的测试用例仅验证旧功能,现在添加了新功能,是否正常工作仍需通过测试来验证:
# UnitTest/unit_test/test/test_city_functions.py
class CitiesTestCase(unittest.TestCase):
"""测试city_functions.py"""
def test_city_country(self):
city_info = get_city_info('shanghai', 'china')
self.assertEqual(city_info, 'Shanghai, China')
def test_New_York_population(self):
city_info = get_city_info('new york', 'America', 8537673)
self.assertEqual(city_info, 'New York, America - 人口: 8537673')
现在新功能的测试用例也已加入,且2个测试均能通过。今后若需对get_city_info()函数进行修改,只需再次运行测试,即可判断新代码是否会影响原有项目。
断言方法
unittest.TestCase类中提供了多种断言方法,前文已使用其中一种。以下是6个常用的断言方法:
- 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中
这些方法只能在继承unittest.TestCase的类中使用。
测试类
前文内容仅涉及对函数的测试。在实际开发中,类的使用更为广泛,因此还需要验证类的正常运行。类的测试与函数测试相似,大部分工作在于测试类中方法的行为,但存在一些差异。
准备要测试的类
首先编写一个待测类,该类存储课程名以及学习该课程的学员列表:
# UnitTest/unit_test/course.py
class CourseManage(object):
def __init__(self, course):
self.course = course
self.students = []
def show_course(self):
print("课程:", self.course)
def add_student(self, name):
self.students.append(name)
def show_students(self):
print("所有学员:")
for student in self.students:
print('-', student)
为了证明CourseManage类工作正常,再编写一个使用该类的程序:
from unit_test.course import CourseManage
course = CourseManage("Python")
course.show_course()
print("准备录入学员...")
print("Enter 'q' at any time to quit.\n")
while True:
resp = input("Student's Name: ")
if resp == 'q':
break
if resp:
course.add_student(resp.title())
print("\n录入完毕...")
course.show_students()
该程序定义了一门课程,并使用课程名创建了一个CourseManage对象。主要操作是调用对象的add_student()方法来录入学员名字。输入完毕后,按q键退出。最后打印所有学员。 所有的输入和输出如下:
课程: Python 准备录入学员... Enter 'q' at any time to quit.
Student's Name: oliver queen Student's Name: barry allen Student's Name: kara Student's Name: sara lance Student's Name: q
录入完毕... 所有学员: - Oliver Queen - Barry Allen - Kara - Sara Lance
Process finished with exit code 0
编写类的测试用例
下面编写一个测试,验证CourseManage类行为的一个方面。如果用户输入了某个学员的名字,这个名字应被存储在self.students列表中。因此,需要在学员录入后,使用assertIn()断言方法:
# UnitTest/unit_test/test/test_course.py
import unittest
from unit_test.course import CourseManage
class TestCourseManage(unittest.TestCase):
def test_add_student(self):
course = CourseManage("Python")
name = 'snart'
course.add_student(name.title())
self.assertIn('Snart', course.students)
if __name__ == '__main__':
unittest.main()
上述方法仅验证了录入单个学员的情况,而大多数情况下涉及多个学员。因此,还需添加一个方法,验证录入多个学员是否正常:
class TestCourseManage(unittest.TestCase):
def test_add_student(self):
course = CourseManage("Python")
name = 'snart'
course.add_student(name.title())
self.assertIn('Snart', course.students)
def test_add_students(self):
course = CourseManage("Python")
name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance']
for name in name_list:
course.add_student(name.title())
for name in name_list:
self.assertIn(name.title(), course.students)
setUp() 方法
在上面的例子中,每个测试方法都创建了一个实例。但还有一种需求:希望只创建一个实例,并在多个方法中对该实例进行操作以反复验证。在unittest.TestCase类中包含setUp()方法,即可实现只实例化一次,并在每个测试方法中使用。如果在TestCase类中定义了setUp()方法,Python会先运行它,再运行各个以test_打头的方法。 简单来说,setUp()方法是父类中预留的一个钩子,会在其他测试方法运行前自动执行:
import unittest
from unit_test.course import CourseManage
class TestCourseManage(unittest.TestCase):
def setUp(self):
self.course = CourseManage("Python")
self.name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance']
def test_add_student(self):
name = 'snart'
self.course.add_student(name.title())
self.assertIn('Snart', self.course.students)
def test_add_students(self):
for name in self.name_list:
self.course.add_student(name.title())
for name in self.name_list:
self.assertIn(name.title(), self.course.students)
if __name__ == '__main__':
unittest.main()
测试自己编写的类时,使用setUp()方法能让测试方法的编写更加轻松,以下是建议的做法:
在setUp()方法中创建一系列实例并设置其属性,然后在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置属性,这种方式要容易得多。
小结
如果在项目中包含初步测试,其他程序员将更敬佩你,他们将能够更得心应手地尝试使用你编写的代码,也更愿意与你合作开发项目。如果你要与其它程序员开发的项目共享代码,就必须证明你编写的代码通过了既有测试,通常还需要为你添加的新行为编写测试。
请通过多开展测试来熟悉代码测试过程。对于自己编写的函数和类,请编写针对其重要行为的测试,但在项目早期,不要试图去编写全覆盖的测试用例,除非有充分的理由这样做。
pytest
本文介绍的是Python内置的单元测试模块。作为初学者,先用着熟悉起来就很不错了。
pytest是Python最流行的单测框架之一。具体可以上GitHub参考那些开源项目的单元测试,很多用的是这个。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













