测试目录
项目的整体结构可以参考 "软件目录开发规范", 这里单说测试目录. 一般都是在项目里单独创建一个测试目录, 目录名就是 "tests".
关于目录的位置, 一种建议是, 在项目名 (假设项目名是 Foo) 的一级子目录下创建二级子目录 "Foo/foo/tests" . 但是这样可能是因为用起来不方便, 有很多是按下面的做法. 不过下面的示例我还是用这个方法来创建测试目录.
还可以把测试目录向上移一层, 作为一级子目录, 直接创建在项目之下 "Foo/tests". 参考 django,scrapy,flask 都是这样的做法.
测试函数
标题的意思是对函数 (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() 进行自动测试, 就能始终确信, 给这个函数提供测试过的姓名后, 它能返回正确的结果. 尤其是在对函数进行修改的前后.
模块导入路径的问题
PyCharm 会自动把项目目录加到环境变量里去, 在 PyCharm 里执行都没问题. 但是如果不用 PyCharm 而是单独运行, 这个目录结构应该会有点问题, 会找不到需要测试的函数. 简单点就是把测试用例和被测试的函数放到同一个目录里, 然后改一下 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() 让 Python 运行这个文件中的测试. 执行程序后得到如下的输出:
- .
- ----------------------------------------------------------------------
- Ran 1 test in 0.000s
- OK
运行测试用例时, 每完成一个单元测试, Python 都打印一个字符:
测试通过时打印一个句点;
测试引发错误时打印一个 E;
测试导致断言失败时打印一个 F.
这就是你运行测试用例时, 在输出的第一行中看到的句点和字符数量各不相同的原因. 如果测试用例包含很多单元测试, 需要运行很长时间, 就可通过观察这些结果来获悉有多少个测试通过了.
PyCharm 对单元测试做了自己的优化, 输出看不到上面的点, 而是有更加漂亮的展示方式.
测试不通过
现在看下测试不通过的效果. 这里不修改测试用例, 而是对 get_city_info() 函数做一个 update, 现在还要显示城市的人口数量:
- 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 而不是之前的点, 表示有一个错误.
测试未通过的处理
这里不要去修改之前的测试用例. 假设 update 之前的函数已经在项目内使用起来了, 现在测试不通过, 表示之前调用这个函数的代码都有问题. 如果不想改项目里其它的代码, 这里先尝试修改 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 参考下那些开源项目的单元测试, 很多用的是这个.
来源: http://blog.51cto.com/steed/2316436