关键字: 单元测试 Python unittest模块
0. 前言
最近在看关于VPP项目中的CSIT的部分,VPP的Path test里主要就是应用了Python的unittest框架,正好在毕业论文项目中也使用到了unittest完成相应单元测试,这篇博客对unittest框架的使用作了全面的介绍,故翻译之作为学习的存档,也算是一个小小的总结。
Python的unittest
框架是基于Kent Beck(TDD测试驱动开发发明人)和Erich Gamma的XUnit框架开发完成的,这种模式在C、Perl、Java和Smalltalk等语言中也被重复利用。unittest
框架中有三个基本概念,分别是test fixtures,test suites以及test runner,这三个概念的组合使用可以实现Python的自动化测试。
1. 基本测试结构
unittest
单元测试主要包含两个部分:用来管理测试依赖环境的代码(这个就是test fixtures),以及测试代码本身。单独的测试可通过继承TestCase
类来覆盖或者增添相应的测试函数。在下面的示例代码中,SimplisticTest
类有仅有一个test()
测试函数,如果变量a
与变量b
不相同,则会导致测试失败。
1 | # unittest_simple.py |
2. 运行测试
运行unittest
单元测试最简单的方式可通过命令行来实现。
1 | $ python3 -m unittest unittest_simple.py |
命令行输出对应测试用例所消耗的时间,以及每个测试用例的测试结果。输出第一行的.
表明该测试用例已通过。我们可以使用-v
指令来查看更加详细版本的测试结果。
1 | $ python3 -m unittest -v unittest_simple.py |
3. 测试结果
unittest
测试结果一共有三种情况,如下表所示。
测试结果 | 相关描述 |
---|---|
OK | 测试通过 |
FAIL | 测试没有通过,触发AssertionError 异常 |
ERROR | 测试触发了除AssertionError 外的异常 |
unittest
中并没有明确的方式来让测试通过,因此一个测试最终的状态取决于是否有异常被触发。
1 | # unittest_outcomes.py |
当测试失败或者产生一个错误时,测试的追踪结果包含在最终的命令行输出中。
1 | $ python3 -m unittest unittest_outcomes.py |
在上述的示例中,testFail()
函数运行失败,命令行输出的追踪结果表明了错误代码的行数。
1 | # unittest_failwithmessage.py |
为了更容易地理解测试代码失败的原因,函数fail*()
和assert*()
都可接受msg
参数,用来显示详细的错误信息。
1 | $ python3 -m unittest -v unittest_failwithmessage.py |
4. 测试是否为真
绝大多数的测试都是在判断一些条件是否为真。unittest
提供了两种不同的编写真值检查测试代码的方法,这两种方法的选择取决于是从测试代码作者的视角还是从待测试代码预期结果的视角。
1 | # unittest_truth.py |
如果代码得到的值为真(True),则使用assertTrue()
方法来验证。如果代码得到的值为假(False),则使用assertFalse()
方法来验证。
1 | $ python3 -m unittest -v unittest_truth.py |
5. 测试是否相等
作为一个特殊用例,unittest
包含测试两值是否相等的方法。
1 | # unittest_equality.py |
当以上测试代码失败时,以上的测试代码将会打印包含被比较值的错误信息。
1 | $ python3 -m unittest -v unittest_equality.py |
6. 测试是否几乎相等
除了严格的相等性,我们可通过assertAlmostEqual()
函数和assertNotAlmostEqual()
函数来测试两个浮点数是否大致相等。
1 | # unittest_almostequal.py |
assertAlmostEqual()
函数的运行流程是先将两个参数进行比较,比较后的绝对值利用round()
函数保存places
参数个小数点后的位置,再查看得到的值是否为0。我的理解assertAlmostEqual(a, b, places=c)
函数可以等价于assertEqual(round(abs(b-a), c), 0)
。
上述代码的测试结果为:
1 | $ python3 -m unittest unittest_almostequal.py |
7. 测试容器类型数据结构
除了常规通用的assertEqual()
和assertNotEqual()
方法,unittest
也提供了针对诸如list
、dict
、set
等容器数据类型的测试方法。
1 | # unittest_equality_container.py |
测试结果如下:
1 | $ python3 -m unittest unittest_equality_container.py |
assertIn()
函数可用来检测元素是否在容器内。
1 | # unittest_in.py |
任何支持in
操作符或者容器API的对象都可以使用assertIn()
方法。
1 | $ python3 -m unittest unittest_in.py |
8. 测试异常
之前我们谈过,如果测试触发了不是AssertionError
的异常,那么这个异常会被当做错误处理。当我们修改已经测试覆盖的代码出现错误时,上述的内容就能够很快帮助我们定位修改产生错误的代码位置。不过在有些场景下,我们需要测试去验证代码确实会产生指定的异常。举个例子,如果一个无效的值赋值给一个对象的属性,那么assertRaises()
函数相比于将异常放在测试代码里处理,则更加清晰。以下面两个测试代码为例做进一步说明:
1 | # unittest_exception.py |
两种测试方法的结果相同,但是使用assertRaises()
方法显得更为简洁,测试结果如下所示。
1 | $ python3 -m unittest -v unittest_exception.py |
9. 测试夹具(Test Fixtures)
夹具是一个测试所需要的外部资源,其目的是搭建一个测试用例的环境以确保测试用例能够按照预期进行。举个例子,对一个类的测试可能需要另一个提供相应配置设置和共享资源的类的实例。还有些情况下的测试夹具则包括数据库连接和相应的临时文件。
unittest
通过设置相应的钩子(hook)来实现测试所需的配置和清理测试夹具的功能。如果想要对每个单独的测试用例创建测试夹具,则需重载TestCase
类中的setUp()
函数,想要清理测试夹具,则重载tearDown()
函数。如果想要管理一个测试类中所有用例的测试夹具,则需重载TestCase
类中的setUpClass()
和setUpClass()
方法。对于模块来说,则可使用setUpModule()
和tearDownModule()
函数。
1 | # unittest_fixtures.py |
测试运行的结果如下所示:
1 | $ python3 -u -m unittest -v unittest_fixtures.py |
可以看到不同的函数的执行位置与相关测试函数的测试流程。有时候在清理测试夹具时会出错,由此导致teardown
方法不被触发。为了确保测试夹具能够被正确释放,,可使用addCleanup()
方法。
1 | # unittest_addcleanup.py |
上述的测试用例创建了一个临时目录,并当测试结束后使用shutil
模块删除临时目录。
1 | $ python3 -u -m unittest -v unittest_addcleanup.py |
10. 针对不同输入重复测试
针对不同输入使用相同的测试逻辑的做法在测试流程中很常用。不同于针对每个小的测试用例都定义一个单独的测试方法,更常规的做法是使用一个包含多个相关断言的测试方法,但是这样会导致只要有一个断言失败,剩下的测试就是被跳过。更好的解决方案是在测试方法内部使用subTest()
来创建测试函数内部上下文,这样如果一个测试失败,终端将会显示相应的失败内容,并且不影响剩余测试的执行。
1 | # unittest_subtest.py |
在上述的代码中,在test_combined()
函数中永远都执行不到'c'
和'd'
的断言,但是test_with_subtest()
方法是可以在'B'
测试失败后继续执行剩下的断言。需要注意的是,上述代码执行的结果虽然报了三处失败,但依旧只是两个测试用例。
1 | $ python3 -m unittest -v unittest_subtest.py |
11. 跳过相关测试
当测试所需的外界条件没有得到满足时,我们常常会跳过相关的测试。可通过skip()
装饰器来跳过测试类和方法,skipIf()
和skipUnless()
装饰器用来在跳过测试前检测相应的条件。
1 | # unittest_skip.py |
对于一些无法用单句表达式传递给skipIf()
或者skipUnless()
函数的复杂情况,测试用例可通过触发SkipTest
异常来达到跳过测试的目的。
1 | $ python3 -m unittest -v unittest_skip.py |
12. 忽略失败的测试用例
我们可通过expectedFailure()
装饰器来忽略注定失败的测试用例。
1 | # unittest_expectedfailure.py |
如果一个测试用例一开始认为测试不通过却在实际测试中通过,这种情况也会被认为是一种特殊形式的测试失败,其终端显示的结果是unexpected failure。
1 | $ python3 -m unittest -v unittest_expectedfailure.py |