Skip to content

单元测试

官方文档:unittest --- 单元测试框架

相关概念

测试固件(test fixture)

表示为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。

测试用例(test case)

一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。

unittest 提供了 TestCase 用于定义一个测试用例。

测试套件(test suite)

一系列的测试用例,或测试套件,或两者皆有。它用于归档需要一起执行的测试。

测试运行器(test runner)

test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。

基本实例

创建一个 python 文件,在内部定义一个继承 TestCase 测试用例:

python
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')
        
if __name__ == '__main__':
    unittest.main()

约束

  • 需要继承 TestCase
  • 测试方法使用 test_ 开头
  • 可以通过直接使用 python 需要测试的 python 文件直接执行
  • 也可以使用 python -m unittest [模块/文件/模块.类/模块.类.方法]...

其它参数:

  • -v:显示更多详细信息

跳过测试与预计的失败

跳过测试

unittest 通过使用如下方法对测试方法进行跳过:

  • @unittest.skip("description"):装饰要跳过的方法
  • @unittest.skipIf(condition, "description"):装饰的方法如果 condition 成立则跳过
  • @unittest.skipUnless(condition, "description"):装饰的方法如果 condition 不成立则跳过
  • 在方法内部调用 self.skipTest("description"):跳过方法

跳过类和跳过方法一致,可在重写 setUp 的方法中执行 skipTest 方法跳过类的执行

预期失败

使用 @unitest.expectFailure 装饰器表示一个测试方法或者类预期是失败的,如果没有失败反而表示是失败。

子测试

通过上下文管理器实现子测试:

python
with self.subTest("descritpion"):
    pass

相关类和函数

TestCase

  • setUp: 此方法会在调用测试方法之前被调用;除了 AssertionError 或 SkipTest,此方法所引发的任何异常都将被视为错误而非测试失败,默认的实现将不做任何事情。
  • tearDown: 在测试方法被调用并记录结果之后立即被调用的方法。 此方法即使在测试方法引发异常时仍会被调用,因此子类中的实现将需要特别注意检查内部状态。 除 AssertionError 或 SkipTest 外,此方法所引发的任何异常都将被视为额外的错误而非测试失败(因而会增加总计错误报告数)。此方法将只在 setUp() 成功执行时被调用,无论测试方法的结果如何。默认的实现将不做任何事情。
  • setUpClass: 在一个单独类中的测试运行之前被调用的类方法。 setUpClass 会被作为唯一的参数在类上调用且必须使用 classmethod() 装饰器。
  • tearDownClass: 在一个单独类的测试完成运行之后被调用的类方法。 tearDownClass 会被作为唯一的参数在类上调用且必须使用 classmethod() 装饰器。
  • skipTest(reason): 跳过测试
  • subTest(msg=None, **params): 子测试

数据驱动测试

数据驱动测试(DDT)使用一组测试数据反复执行同一测试用例。

ddt

常用装饰器:

  • @ddt: 用于类级别,启用整个类的数据驱动功能。
  • @data: 用于方法级别,指定要进行数据驱动测试的方法。
  • @unpack: 用于方法级别,指定方法需要对方法参数进行解包。

简单使用:

python
import unittest
from ddt import ddt, data, unpack

def add(a, b):
    return a + b

@ddt
class TestAddFunction(unittest.TestCase):
    
    @data((1, 2, 3), (2, 3, 5), (10, 5, 15))
    @unpack
    def test_add(self, a, b, expected):
        result = add(a, b)
        self.assertEqual(result, expected)

if __name__ == "__main__":
    unittest.main()

✨ 原理

使用 @data@unpack 装饰器会给方法添加而外的属性。 然后 @ddt 装饰器类会去检查类的所有方法,如果有这个属性,就会通过这些属性进行处理,从而生成多个测试方法,并删除原测试方法。

手动实现

通过 @data 将数据设置到对应方法的属性内,然后通过 @ddt 生成测试方法,这里的测试方法直接追加的序号,ddt 库的默认实现是追加序号和参数。

循环和闭包的问题

python
import functools
import unittest

DATA_ATTR = "%data"


def data(*values: tuple):
    def decorator(func):
        setattr(func, DATA_ATTR, values)
        return func

    return decorator

def wrap_func(func, value):

    @functools.wraps(func)
    def wrapper(self):
        func(self, value)
        pass

    return wrapper

def ddt(cls):
    for name, func in list(cls.__dict__.items()):
        if hasattr(func, DATA_ATTR):
            for i, value in enumerate(getattr(func, DATA_ATTR)):
                setattr(cls, f"{name}_{i}", wrap_func(func, value))

            # 删除原方法
            delattr(cls, name)
    print(cls.__dict__.items())

    return cls


def add(a, b):
    return a + b


@ddt
class TestAddFunction(unittest.TestCase):

    @data((1, 2, 3), (2, 3, 5), (10, 5, 15))
    def test_add(self, param):
        a, b, expected = param
        result = add(a, b)
        self.assertEqual(result, expected)


if __name__ == "__main__":
    unittest.main()