Python 单元测试框架
前言
这几天在看 jaeger-client-python 的测试部分,主要使用 unittest 和 pytest 构造测试,使用 mock 辅助测试。以下内容主要包含 jaeger-client-python 涉及的一些使用方式,unittest 是 Python 自带的单元测试框架,pytest 兼容 unittest 和 nose 测试集,并有丰富的插件支持。
unittest
构造测试
继承 unittest.TestCase
就创建了测试样例,被测方法以 test
开头命名。使用 assert*()
方法能够聚合所有测试结果并产生报告,unittest.main()
提供了测试脚本的命令行接口。
1 | import unittest |
前置方法与后置方法
测试的前置操作可以实现在测试前置方法 setUp()
中,在运行测试时,测试框架会自动为每个单独测试调用前置方法。tearDown()
方法能够在测试方法运行后进行清理工作。
一个测试代码运行的环境被称为 test fixture 。一个新的 TestCase 实例作为一个测试脚手架,用于运行各个独立的测试方法。在运行每个测试时,setUp()
、tearDown()
和 __init__()
会被调用一次。
setUpClass()
和 tearDownClass()
在测试类中只执行一次,而不是每个测试方法前后都执行。
setupModule()
和 tearDownModule()
在单个测试模块中只执行一次。
命令行界面
unittest 模块可以通过命令行运行模块、类和独立测试方法的测试。
1 | # 测试模块 |
test_api.py 是 jaeger-python-client 中的测试。
组织测试
test suite 机制支持将测试用的 TestCase 实现集合起来,自定义测试套件可以参考以下方法组织测试
1 | def suite(): |
FunctionTestCase 类可用于打包已有的测试函数,并支持设置前置与后置函数,假定如下测试函数:
1 | def testSomething(): |
可以通过 FunctionTestCase 类创建等价的测试用例,其中前置与后置函数是可选的:
1 | testcase = unittest.FunctionTestCase(testSomething, |
跳过测试
unittest 提供了几个装饰器用于跳过测试:
1 | class MyTestCase(unittest.TestCase): |
子测试
在一个测试中,传入不同的参数测试同一个方法,subTest
子测试可以满足这个需求,而且单个子测试的失败不影响后续子测试的执行。
1 | import unittest |
pytest
在测试函数中使用 assert
断言进行条件判断,然后使用 pytest 执行测试。
1 | def test_passing(): |
.
表示 PASSED
,F
表示 FAILED
,使用 -v
显示测试的详细信息
1 | def test_failing(): |
捕获异常
使用 pytest.raises()
进行异常捕获:
1 | import pytest |
执行测试
默认情况下,pytest 会递归查找当前目录下所有以 test
开始或结尾的 Python 脚本,并执行文件内的所有以 test
开始或结束的函数和方法。直接执行测试脚本会同时执行所有测试函数。
1 | pytest test_no_mark.py |
这里的 .F
表示第一个函数执行成功,第二个函数执行失败
如果想要指定测试函数,可以使用以下几种方法:
- 显式指定函数名,通过
::
标记。缺点是一次只能执行一个测试函数。
1 | pytest test_no_mark.py::test_func1 -v |
- 模糊匹配,使用
-k
选项标识。可以批量操作,但需要被测函数包含相同的模式。
1 | pytest -k func1 test_no_mark.py -v |
- 使用
pytest.mark
在函数上进行标记
首先,需要对标记进行注册,在配置文件 pytest.ini 中注册标记
1 | [pytest] |
然后在代码中使用标记
1 | import pytest |
测试时使用 -m
选择标记的函数
1 | pytest -m finished test_with_mark.py -v |
pytest 内置了一些标记:
- usefixtures - 在测试函数或类上使用 fixture
- filterwarnings - 过滤测试函数的某些警告
- skip - 总是跳过测试函数
- skipif - 如果满足某个条件,则跳过测试函数
- xfail - 如果满足某个条件,则产生“预期失败”结果
- parametrize - 对同一测试函数执行多个调用
固定装置/固件(fixture)
使用 @pytest.fixture
将函数定义为固定装置(固件),最简单的使用方式是作为测试函数的参数 。使用固定装置的好处是可以重复使用、装置可以请求其他装置等。
1 | import pytest |
固定装置可以设置范围(scope),可能的值包括:
function
:默认范围,每个测试函数都会执行一次 fixture。class
:每个测试类执行一次 fixture,所有方法都可以使用。module
:每个模块执行一次 fixture,模块内函数和方法都可以使用。package
:每个包执行一次 fixture,包内的模块、函数、方法都可以使用。session
:一次测试只执行一次 fixture,所有被找到的函数和方法都可用。
pytest 一次只缓存 fixture 的一个实例,这意味着当使用参数化 fixture 时,pytest 可以在给定范围内多次调用 fixture 。
清理测试可以使用 pytest 提供的拆卸系统,允许自定义每个固定装置清理所需的具体步骤,通过以下两种方式使用:
- yield 语句
pytest 使用 yield
关键词将固件分为两部分,yield
之前的代码属于预处理,会在测试前执行;yield
之后的代码属于后处理,将在测试完成后执行。
使用 yield 语句执行拆卸,以前的写法是 @pytest.yield_fixture
,现在可以直接在 @pytest.fixture
装饰的函数中使用 yield 语句。
1 | import pytest |
- 添加 finalizer
接受 request 对象,通过 addfinalizer
方法添加拆卸函数,必须在完成必要的拆卸工作后再进行添加,否则可能引发异常。
1 | import pytest |
参数化测试
使用 @pytest.mark.parametrize
装饰器实现参数化测试功能。这里定义了三个不同的 (test_input,expected)
,因此 test_eval 将运行三次。
1 | import pytest |
其他
@pytest.mark.gen_test
由 pytest-tornado 提供,该标记允许编写协程风格的测试与 tornado.gen 模块一起使用,也支持 async/await(同步/阻塞)的语法。
1 | import pytest |