单元测试
官方文档: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()