声明:前面的python标准库系列文章详细讲解了Python的三大函数式编程库以及集合库,itertools、functools、operators库以及collections库,本文继续python标准库系列文章,本文为第五篇,深入详解python的单元测试原理以及unittest的基本架构,鉴于篇幅较长,第五篇将分为几个小的篇章说明,本文为上篇,后续还会有系列文章更新,希望对小伙伴有所帮助!
一、python单元测试的基本概念
1、什么是“单元测试”
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
2、什么是unittest
unittest原名为PyUnit,是由java的JUnit衍生而来。unittest是xUnit系列框架中的一员,如果你了解xUnit的其他成员,那你用unittest来应该是很轻松的,它们的工作方式都差不多。对于单元测试,需要设置预先条件,对比预期结果和实际结果。为什么要这么做呢?
因为随着项目的不断扩大,单元测试在保证开发效率、可维护性和软件质量等方面的地位越发举足轻重,是一本万利的举措,单元测试保证我们所写的“单元”预期结果与真实结果是不是一样的,从而有利于我们发现错误。
python的unittest库是python自带的标准单元测试库,无需再自己安装。因为python语言一切皆对象,故而最小单元可以是“函数、类、模块”。
3、unittest的基本架构
先从unittest的五大基本概念说起:
框架的五个概念: 即test case、test suite、testLoader、test runner、test fixture。
(1)test case :一个完整的测试单元,执行该测试单元可以完成对某一个问题的验证,完整体现在:测试前环境准备(setUp),执行测试代码(run),以及测试后环境还原(tearDown);
(2)test suite :多个测试用例的集合,测试套件或测试计划;
(3)testLoader :加载TestCase到TestSuite中的,其中loadTestsFrom__()方法用于寻找TestCase,并创建它们的实例,然后添加到TestSuite中,返回TestSuite实例;
(4)test runner :执行测试用例,并将测试结果保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息;
(5)test fixture:一个测试用例的初始化准备及环境还原,主要是setUp() 和 setDown()方法;
这是unittest里面的5大核心构件,后面会一个一个进行讲解的。
4、unittest的工作流程
通过unittest类调用分析,可将框架的工作流程概况如下:
编写TestCase,由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite, 最后将运行的结果保存在TextTestResult中。
二、unittest的简单实例——TestCase
1、使用unittest实现一个最基础的单元测试
比如我有一个模块functions,里面定义了4个函数分别实现加减乘除,现在我要测试这四个函数的正确与否。
def add(a, b):
return a+b
def minus(a, b):
return a-b
def multi(a, b):
return a*b
def divide(a, b):
return a/b
现在我要测试上面的四个方法,我们可以这样做,新建一个Python文件test.py,代码如下:
from functions import *
import unittest
class TestFunctions(unittest.TestCase):
def test_add(self): #测试加法的方法
self.assertEqual(3, add(1, 2))
self.assertNotEqual(3, add(2, 2))
def test_minus(self): #测试减法的方法
self.assertEqual(1, minus(3, 2))
def test_multi(self): #测试乘法的方法
self.assertEqual(6, multi(2, 3))
def test_divide(self): #测试除法的方法
self.assertEqual(2, divide(6, 3))
self.assertEqual(2.5, divide(5, 2))
if __name__ == '__main__':
unittest.main()
然后运行上面的test.py这个模块,即
python test.py 或者是
python -m unittest test.py (unittest模块是可以当做script去执行的哦)
打印出结果如下:
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
这里为什么会有这么多点,什么意思后面会讲解的。
单元测试的一般步骤总结:
第一步:选定需要测试的函数。比如上面的加减乘除四个方法;
第二步:定义一个测试类(核心)。继承自unittest.TestCase类,然后在类中定义实例测试方法,这里需要注意的是,测试方法一定要以“test”开头,表示这是一个测试方法,否则是不会进行运行的,当然为了习惯性地表示,和便于查看,一般就写成“test_被测试方法名”这种形式了,但是后面的下划线和被测试方法名称不是强制性的,只要保证是test开头的即可,否则是没有办法被unittest识别的哦。
第三步:通过unittest.main()来运行测试类。这里我们发现,没有构造自定义测试类的对象,更没有显示调用某个方法,但他就是运行了哦。main函数有一个verbosity
参数,该参数可以控制输出的错误报告的详细程度,默认是 1
,如果设为 0
,则不输出每一用例的执行结果,即没有上面的结果中的第1行(那几个点);如果设为 2,
则输出详细的执行结果。
上面是一个python单元测试的一般步骤,当然还有很多其他的东西后面会讲到,因为我们将unittest有5大构建,这里还只讲了一个TestCase。
注意:上面的运行结果中,有四个点.... 那么是什么意思呢?
成功是 .
,失败是 F
,出错是 E
,跳过是 S
2、单元测试的详细信息输出
上面的运行结果实在是太简单了,就几个点,我啥也看不明白,我们可以给每一个测试函数添加函数注释,并且在运行结果中显示出来,如下:
from functions import *
import unittest
class TestFunctions(unittest.TestCase):
"""这是其自己编写测试类"""
def test_add(self): #测试加法的方法
"""这是测试add的测试方法"""
self.assertEqual(3, add(1, 2))
self.assertNotEqual(3, add(2, 2))
def test_minus(self): #测试减法的方法
"""这是测试minus的测试方法"""
self.assertEqual(1, minus(3, 2))
def test_multi(self): #测试乘法的方法
"""这是测试multi的测试方法"""
self.assertEqual(6, multi(2, 3))
def test_divide(self): #测试除法的方法
"""这是测试divide的测试方法"""
self.assertEqual(2, divide(6, 3))
self.assertEqual(2.5, divide(5, 2))
if __name__ == '__main__':
unittest.main(verbosity=2)
运行结果为:
test_add (__main__.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (__main__.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (__main__.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (__main__.TestFunctions)
这是测试multi的测试方法 ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.005s
OK
从上面可以看出,测试函数中的注释内容也打印了出来,即合理的使用注释和描述,能够使输出的测试报告更加便于阅读。
三、测试函数的组织——TestSuite
概念声明:在上面的例子中,自定义的TestFunctions是一个测试类,里面的每一个测试方法,即以test开头的方法,称之为一个Case。
反复运行上面的测试代码test.py,我们发现一个问题,我们所要测试的四个函数 add minus multi divide 它们每次在被测试的时候,被测试的顺序是不确定的,并时不时按照我在测试类中的定义那么顺序来执行的,可能是 add minus multi divide,也可能是add divide minus multi 等等。
这个时候我们就要用到TestSuite了。我们添加到TestSuite中的Case是会按照添加的顺序执行的。其实顾名思义,Suite的英文含义就是“套件、套装”,即由某一种规则或者是约束的东西。(注意:我们每一个要测试的方法称之为一个Case)
不仅如此,上面的例子中只有一个测试文件即test.py,我们直接执行该文件即可,但如果有多个测试文件,怎么进行组织,总不能一个个文件执行吧,多个测试类也是通过TestSuite来完成的。
所以,TestSuite是用来解决两类问题的:
第一:顺序测试问题
第二:多个测试类问题(不需要运行每一个测试脚本)
1、使用TestSuite进行Case的顺序组织
import unittest
from test import TestFunctions #导入我自己定义的测试类
if __name__ == '__main__':
# 第一步:构建需要测试的测试方法列表,按照列表中的顺序进行测试,不在列表中的则不进行测试
tests = [TestFunctions("test_add"), TestFunctions("test_minus"), TestFunctions("test_divide")]
# 第二步:构建TestSuite对象,并将Case列表添加进去
suite = unittest.TestSuite()
suite.addTests(tests)
#第三步:构建一个TextTestRunner对象,并且运行第二步中的suite对象
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
运行结果为:
test_add (test.TestFunctions)
这是测试add的测试方法 ... ok
test_minus (test.TestFunctions)
这是测试minus的测试方法 ... ok
test_divide (test.TestFunctions)
这是测试divide的测试方法 ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
从上面的执行结果可以看出来,suite里面的三个Case是按照顺序进行测试的,且没有添加进来的就不会测试。
注意:上面是先将Case组织成一个列表,然后一次性添加进Suite,我也可以以一个一个添加的,即:
suite.addTest(TestFunctions("test_multi")) #将test_multi测试函数添加进去。
总结:三步走
第一步:构建需要测试的测试方法列表,按照列表中的顺序进行测试,不在列表中的则不进行测试
第二步:构建TestSuite对象,并将Case列表添加进去
第三步:构建一个TextTestRunner对象,并且运行第二步中的suite对象
2、使用TestLoader将要测试的Case添加进TestSuite里面
上面的例子,不管是一次性将所有的Case全部添加进Suite,还是一个一个添加,都是有序的,除此之外,我还可以使用TestLoader对象,这个也是unittest的5大核心构件之一,特也可以将Case加载到Suite里面去,代码如下:
import unittest
from test import TestFunctions #导入我自己定义的测试类
# 第一步:构造一个TestSuite对象
suite = unittest.TestSuite()
#第二步:构造一个TestLoader对象
loader=unittest.TestLoader()
#第三步:通过loader将所有的Case传递到Suite里面,故而没有先后顺序
suite.addTests(loader.loadTestsFromName('test.TestFunctions'))
#第四步:构建一个TextTestRunner对象,并且运行第二步中的suite对象
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
运行结果为:
test_add (test.TestFunctions)
这是测试add的测试方法 ... ok
test_divide (test.TestFunctions)
这是测试divide的测试方法 ... ok
test_minus (test.TestFunctions)
这是测试minus的测试方法 ... ok
test_multi (test.TestFunctions)
这是测试multi的测试方法 ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
从上面可知,使用TestLoader加载的依然是没有顺序的,为什么呢?因为他不是按照顺序把每一个Case添加进suite,而是一次性将一个或者是好几个TestCase中的全部Case全部添加进去,至于顺序到底是怎样的,没办法确定。
总结:四步走
第一步:构造一个TestSuite对象
第二步:构造一个TestLoader对象
第三步:通过loader将TestFunctions里面所有的Case传递到Suite里面,故而是没有顺序的
第四步:构建一个TextTestRunner对象,并且运行第二步中的suite对象
3、两个最核心的的方法讲解
上面那么多的这步骤,这哪里记得住啊,其实完全没有必要死记硬背,只需要掌握两组核心方法就可以了
(1)suite.addTests()和suite.addTest()
这两个方法是实现将Case添加进TestSuite的功能,
a)、直接用addTest方法添加单个Case
suite.addTest(TestFunctions("test_multi")) ,即suite.addTest(自定义测试类("以test开头的测试方法名"))
b)、直接用addTests添加过个Case或者是所有的Case
tests = [TestFunctions("test_add"), TestFunctions("test_minus"), TestFunctions("test_divide")]
suite.addTests(tests) #添加多个Case
也可以是
suite.addTests(loader.loadTestFrom***()),即它的参数也可以是loader的方法所返回的一个suite对象。
(2)loader.loadTestFrom***()系列方法——它包含四个方法
a)、loader.loadTestsFromModul(module) #参数为一个模块名,如本文中的test。它返回一个模块中所包含的所有的Cases组成的suite
b)、loader.loadTestsFromName('test.TestFunctions')) #参数为一个测试类名,但是要写成字符串的形式。它返回给定的测试类(如TestFunctions)中的所有的Cases所组成的suite。
c)、loader.loadTestsFromNames(['test.TestFunctions']) #参数为一个列表,列表的每一个元素都是由测试类名所组成的字符串。它返回一系列的测试类中所包含的所有的Cases所组成的suite.(这就解决多个测试类的问题,机上面所说的问题二)
d)、loader.loadTestsFromTestCase(TestFunctions) #参数为一个测试类,类中的所有的Cases所组成的suite。
四、unittest单元测试总结:
1、单元测试的一般流程:(三步走)
第一步:编写TestCase;
第二步:由TestLoader加载TestCase到TestSuite(这是无序的),也可以自己使用有序的方法,参见上面;
第三步:然后由TextTestRunner来运行TestSuite, 最后将运行的结果保存在TextTestResult中。关于TextTestResult会在系列文章的后面进行讲解。
2、编写单元测试方法(以test开头)的常用断言方法Assertion
unittest库提供了很多实用方法来检测程序运行的结果和预期。包括三种类型的方法,每一种都覆盖了典型的类型,比如:
检查相等值、逻辑比较、异常
如果给定的Assertion通过了,那么测试会执行下一行代码。如果给定的assertion没有通过,测试会暂停并且生成错误信息。unittest库提供所有标准的xUnit assert方法。下面列出较重要方法的一部分:
Method | Checks that | New in |
---|---|---|
assertEqual(a, b) | a == b | |
assertNotEqual(a, b) | a != b | |
assertTrue(x) | bool(x) is True | |
assertFalse(x) | bool(x) is False | |
assertIs(a, b) | a is b | 3.1 |
assertIsNot(a, b) | a is not b | 3.1 |
assertIsNone(x) | x is None | 3.1 |
assertIsNotNone(x) | x is not None | 3.1 |
assertIn(a, b) | a in b | 3.1 |
assertNotIn(a, b) | a not in b | 3.1 |
assertIsInstance(a, b) | isinstance(a, b) | 3.2 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 3.2 |
Method | Checks that | New in |
---|---|---|
assertRaises(exc, fun, *args, **kwds) | fun(*args, **kwds) raises exc | |
assertRaisesRegex(exc, r, fun, *args, **kwds) | fun(*args, **kwds) raises exc and the message matches regex r | 3.1 |
assertWarns(warn, fun, *args, **kwds) | fun(*args, **kwds) raises warn | 3.2 |
assertWarnsRegex(warn, r, fun, *args, **kwds) | fun(*args, **kwds) raises warn and the message matches regex r | 3.2 |
assertLogs(logger, level) | The with block logs on logger with minimum level | 3.4 |
Method | Checks that | New in |
---|---|---|
assertAlmostEqual(a, b) | round(a-b, 7) == 0 | |
assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 | |
assertGreater(a, b) | a > b | 3.1 |
assertGreaterEqual(a, b) | a >= b | 3.1 |
assertLess(a, b) | a < b | 3.1 |
assertLessEqual(a, b) | a <= b | 3.1 |
assertRegex(s, r) | r.search(s) | 3.1 |
assertNotRegex(s, r) | not r.search(s) | 3.2 |
assertCountEqual(a, b) | a and b have the same elements in the same number, regardless of their order | 3.2 |
Method | Used to compare | New in |
---|---|---|
assertMultiLineEqual(a, b) | strings | 3.1 |
assertSequenceEqual(a, b) | sequences | 3.1 |
assertListEqual(a, b) | lists | 3.1 |
assertTupleEqual(a, b) | tuples | 3.1 |
assertSetEqual(a, b) | sets or frozensets | 3.1 |
assertDictEqual(a, b) | dicts | 3.1 |
Method Name | Deprecated alias | Deprecated alias |
---|---|---|
assertEqual() | failUnlessEqual | assertEquals |
assertNotEqual() | failIfEqual | assertNotEquals |
assertTrue() | failUnless | assert_ |
assertFalse() | failIf | |
assertRaises() | failUnlessRaises | |
assertAlmostEqual() | failUnlessAlmostEqual | assertAlmostEquals |
assertNotAlmostEqual() | failIfAlmostEqual | assertNotAlmostEquals |
assertRegex() | assertRegexpMatches | |
assertNotRegex() | assertNotRegexpMatches | |
assertRaisesRegex() | assertRaisesRegexp |
五、下一篇预告
限于篇幅,下一篇会接着本篇内容详细讲解TestRunner、TestFixture相关的内容,有兴趣可以继续关注。