# 关于测试

# 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 的标准库,用于进行一些检查。

  • 官方文档 (opens new window)

  • 检查对象的类型:

    >>> 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 的标准库,用于进行单元测试。

# 示例

  1. 在 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
    
    
  2. 调用 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 的第三方库,用于进行单元测试。

# 编写

  • 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']))