# 关于测试
# import traceback
:Python 的标准库,用于读取 traceback 。
- 官方文档 (opens new window)
- Python 解释器会将抛出的异常,暂存在 traceback 堆栈中。因此,可以通过 traceback 读取当前的异常。
- 例:
>>> import traceback >>> try: ... raise RuntimeError('an error') ... except: ... traceback.print_exc() # 打印当前的所有异常 ... info = traceback.format_exc() # 保存异常的内容 ... Traceback (most recent call last): File "<stdin>", line 2, in <module> RuntimeError: an error >>> info 'Traceback (most recent call last):\n File "<stdin>", line 2, in <module>\nRuntimeError: an error\n' >>> traceback.format_exc() # 之前的异常已经被 except 捕捉,此时 traceback 中不存在异常 'NoneType: None\n'
# import inspect
:Python 的标准库,用于进行一些检查。
检查对象的类型:
>>> import inspect >>> inspect.ismodule(inspect) True >>> inspect.isclass(int) True >>> inspect.isfunction(print) # 检查对象是否为函数,不能检查 built-in 函数 False >>> inspect.isfunction(lambda : ...) True >>> inspect.isgenerator((i for i in range(5))) # 检查对象是否为生成器 True
检查源代码:
>>> inspect.getfile(inspect.getfile) # 找到该对象是在哪个 .py 文件中创建的 '/usr/lib64/python3.6/inspect.py' >>> inspect.getfile(int) # 不能查找 built-in 的对象 TypeError: <class 'int'> is a built-in class >>> inspect.getsourcelines(inspect.getfile) # 返回该对象的源代码 (['def getfile(object):\n', ' """Work out which source or compiled file an object was defined in."""\n', ' if ismodule(object):\n', " ...], 655)
检查函数形参:
>>> def fun1(a, b: str, c: int = 0): ... pass ... >>> sign = inspect.signature(fun1) # 查看函数的调用签名,返回一个 Signature 对象 >>> sign <Signature (a, b: str, c: int = 0)> >>> sign.parameters # 获取函数的形参列表,返回一个 Map 对象 mappingproxy(OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b: str">), ('c', <Parameter "c: int = 0">)])) >>> sign.parameters['c'].name 'c' >>> sign.parameters['c'].default 0 >>> sign.parameters['c'].annotation <class 'int'>
>>> sign.bind(1) # 可以模拟给函数传入参数 TypeError: missing a required argument: 'b' >>> sign.bind(1, b=2) <BoundArguments (a=1, b=2)> >>> _.args # 获取元组类型的所有参数 (1, 2)
# import unittest
:Python 的标准库,用于进行单元测试。
- 官方文档 (opens new window)
- 它是一个单元测试框架,借鉴 Java 的 JUnit 。
# 示例
在 Python 脚本中,编写 unittest 测试方法,又称为测试用例:
import unittest class TestMath(unittest.TestCase): # 定义一个继承 unittest.TestCase 的类 def test_add(self): # 如果方法名匹配 test* ,则会被视作一个 unittest 测试用例 assert 1 + 1 def test_minus(self): assert 1 - 1
调用 unittest 模块,自动发现所有 unittest 测试用例,并执行它们:
[root@CentOS ~]# python3 -m unittest -v test_add (__main__.TestMath) ... ok test_minus (__main__.TestMath) ... FAIL ====================================================================== FAIL: test_minus (__main__.TestMath) ---------------------------------------------------------------------- Traceback (most recent call last): File "testSample.py", line 8, in test_minus assert 1 - 1 AssertionError ---------------------------------------------------------------------- Ran 2 tests in 0.002s FAILED (failures=1)
- 默认是在当前目录下,查找名称匹配
test*.py
的所有文件,自动发现其中的 unittest 测试用例。 - 如果当前目录的子目录,属于 Python 包,则也加入查找范围。
- 执行每个测试用例时,
- 如果没有抛出异常,则算这项测试通过,显示 ok 。
- 如果抛出异常,则算这项测试失败,显示 FAIL 。等 unittest 执行完所有测试用例,才会打印抛出的各个异常。
- 默认是在当前目录下,查找名称匹配
# import pytest
:Python 的第三方库,用于进行单元测试。
- 官方文档 (opens new window)
- 安装:
pip install pytest
- pytest 比 unittest 的功能更多,而且可以兼容地执行 unittest 的测试用例。
# 编写
pytest 要求,将包含测试用例的 Python 脚本,命名为
test_*.py
或*_test.py
。- 这些脚本中,名称匹配
test_*
的函数,会被视作测试用例。 - 这些脚本中,名称匹配
Test*
的类,会被视作测试类。- 该类中,名称匹配
test_*
的方法,会被视作测试用例。 - 该类不能定义
__init__()
方法。
- 该类中,名称匹配
- 例:
def test_a(): assert 1 > 2 class TestClass: def test_b(self): assert 1 > 2 def test_c(self): assert 1 > 2
- 这些脚本中,名称匹配
用
with pytest.raises()
可以检查是否抛出了指定异常。如果抛出了,则测试结果为 pass 。如果没抛出,则测试结果为 fail 。def test_fun1(): with pytest.raises(KeyboardInterrupt): raise KeyboardInterrupt
pytest 支持定义以下钩子函数:
setup_module() # 如果一个 Python 模块包含测试用例,则在执行这些测试用例之前,自动调用一次该名称的函数 teardown_module() # 执行一个 Python 模块的全部测试用例之后,自动调用一次该函数 setup_function() # 执行每个测试函数之前,自动调用一次该名称的函数 teardown_function() setup_class() # 如果一个测试类包含测试用例,则在执行这些测试用例之前,自动调用一次该名称的方法 teardown_class() setup_method() # 执行每个测试方法之前,自动调用一次该名称的方法 teardown_method()
可以通过装饰器
@pytest.fixture
,定义更灵活的钩子函数。- 例:
import pytest @pytest.fixture # 定义一个 fixture 函数 def setup_1(): return 1 @pytest.fixture def setup_2(): return 2 def test_a(setup_1, setup_2): # 在测试用例的形参列表中,可以采用任意个 fixture 函数,它们会按从右到左的顺序执行 print(setup_1) # 每次执行该测试用例之前,会调用一次这些 fixture 函数,并将它们的返回值,保存为实参 print(setup_2)
- 可以在 fixture 函数中使用 yield 返回值。这样 yield 之前的代码,会在测试用例开始时执行。yield 之后的代码,会在测试用例结束时执行。
@pytest.fixture def setup_1(): print('setup') yield 1 print('teardown')
- 可以给 fixture 装饰器,传入 params 参数。这样执行每个测试用例时,会执行多次,每次传入不同的参数给 fixture 函数。
@pytest.fixture(params=[1, 2, 3]) def setup_1(request): return request.param def test_a(setup_1): print(setup_1) # 这里会执行 3 次,依次打印 1、2、3
- 可以给 fixture 装饰器,传入 scope 参数。这样 fixture 函数在同一个作用域,只会被执行一次,然后将返回值给多个测试用例复用。
@pytest.fixture(scope='module') # scope 可以取值为 class、module、session 等 def setup_1(): import random return random.randint(1, 10) def test_a(setup_1): print(setup_1) def test_b(setup_1): print(setup_1)
- 例:
# 执行
使用 pytest 命令,即可执行测试用例。用法如下:
pytest [path]... # 指定文件或目录(默认是当前目录),让 pytest 执行其中所有测试用例 -k <pattern> # 只执行名称包含 pattern 的测试用例 -q # 只显示简短的测试结果 -v # 详细地显示测试结果 -s # 显示测试用例的 stdout
- 显示的测试结果中,
F
表示 fail ,E
表示抛出了 Exception 。
- 显示的测试结果中,
也可调用
pytest.main()
来执行 pytest ,支持传入命令参数。import pytest if __name__ =="__main__": exit(pytest.main(['-v']))