# 0. 前言
ddt 是 python 的第三方库,主要是解决使用 unittest 来写单测时可以支持参数化的配置,这个库的使用方法可以参考我之前写的使用ddt实现unittest的参数化测试 (opens new window)。本文主要是讲自己在学习 ddt 库时所获。
ddt 库的使用方法是用装饰器来实现的,可以参考这边文章python装饰器的使用方法 (opens new window)来学习装饰器.
# 1. 源码分析
# 1.1 example
先看一个最简单的使用例子,我们创建 larger_than_two
函数,并使用 unittest 对其编写单测。
这里使用了 @ddt
来装饰 DemoTestCase,并使用 @data
填写多个测试的参数,这样执行就完成了参数化的单测了。
import unittest
from ddt import ddt, data
def larger_than_two(value):
return value > 2
@ddt
class DemoTestCase(unittest.TestCase):
@data(1, 2, 3)
def test_larger_than_two(self, value):
self.assertTrue(larger_than_two(value))
我们执行上面的单测会发现,虽然我们代码只写了一个用例,但是执行却是 3 个用例,成功了 1 个,失败了 2 个,并且输出了失败的用例的名称,test_larger_than_two_1_1
和 test_larger_than_two_2_2
,名称的规则是:单测的名称_索引_参数
。
FF.
======================================================================
FAIL: test_larger_than_two_1_1 (__main__.DemoTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "c:crazyboycodeddtddt.py", line 220, in wrapper
return func(self, *args, **kwargs)
File "C:CrazyBoyworkspacedemodemo.py", line 24, in test_larger_than_two
self.assertTrue(larger_than_two(value))
AssertionError: False is not true
======================================================================
FAIL: test_larger_than_two_2_2 (__main__.DemoTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "c:crazyboycodeddtddt.py", line 220, in wrapper
return func(self, *args, **kwargs)
File "C:CrazyBoyworkspacedemodemo.py", line 24, in test_larger_than_two
self.assertTrue(larger_than_two(value))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 3 tests in 0.004s
FAILED (failures=2)
Process finished with exit code 1
这是如何实现的呢?
# 1.2 源码分析流程
我们首先来看看 @data 装饰器里面做了什么?
代码语言:javascript复制def data(*values):
return idata(values)
data 调用了函数 idata,我们再来看看 idata 的实现,通过 setattr 方法,给被装饰的单测用例添加两个属性
- DATA_ATTR 是用来保存 data 的参数化的参数。
- INDEX_LEN 用来保存参数化的长度。
DATA_ATTR = '%values'
INDEX_LEN = '%index_len'
def idata(iterable, index_len=None):
if index_len is None:
iterable = tuple(iterable)
index_len = len(str(len(iterable)))
def wrapper(func):
setattr(func, DATA_ATTR, iterable)
setattr(func, INDEX_LEN, index_len)
return func
return wrapper
然后我们再来看装饰器@ddt 中,传入的 cls 是被装饰的单测类,通过该类,找到上面使用@data 装饰器中添加的属性 DATA_ATTR 和对应的单测方法,其中的每条数据都是一个用例,通过遍历该属性中的参数值调用函数 mk_test_name
去构造每一条参数的用例名称。
然后再调用 add_test 函数去生成对应的单测用例。
代码语言:javascript复制def ddt(arg=None, **kwargs):
fmt_test_name = kwargs.get("testNameFormat", TestNameFormat.DEFAULT)
def wrapper(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
index_len = getattr(func, INDEX_LEN)
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(
name,
getattr(v, "__name__", v),
i,
index_len,
fmt_test_name
)
test_data_docstring = _get_test_data_docstring(func, v)
if hasattr(func, UNPACK_ATTR):
if isinstance(v, tuple) or isinstance(v, list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
else:
# unpack dictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
add_test(cls, test_name, test_data_docstring, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)
delattr(cls, name)
return cls
# ``arg`` is the unittest's test class when decorating with ``@ddt`` while
# it is ``None`` when decorating a test class with ``@ddt(k=v)``.
return wrapper(arg) if inspect.isclass(arg) else wrapper
我们看看 add_test
做了什么?很简单,就是给单测的 TestCase 添加属性,以单测用例名称为名,feed_data 的返回值为值。
feed_data 中,根据单个参数值和被@data 装饰的函数组成一个新的单测用例,并返回出去。
代码语言:javascript复制def add_test(cls, test_name, test_docstring, func, *args, **kwargs):
setattr(cls, test_name, feed_data(func, test_name, test_docstring, *args, **kwargs))
def feed_data(func, new_name, test_data_docstring, *args, **kwargs):
@wraps(func)
def wrapper(self):
return func(self, *args, **kwargs)
wrapper.__name__ = new_name
wrapper.__wrapped__ = func
# set docstring if exists
if test_data_docstring is not None:
wrapper.__doc__ = test_data_docstring
else:
# Try to call format on the docstring
if func.__doc__:
try:
wrapper.__doc__ = func.__doc__.format(*args, **kwargs)
except (IndexError, KeyError):
pass
return wrapper
也就是说,参数化的每个值都会生成一个用例方法并注册到被@ddt 装饰的 TestCase 类中。
# 2. 总结
主要流程是:通过 @data 装饰器将参数化注册到该单测用例方法的 DATA_ATTR 属性中,然后@ddt 装饰器遍历当前 TestCase 的所有包含 DATA_ATTR 属性的用例方法,再遍历其 DATA_ATTR 的参数值,把每条参数值都生成一条用例方法,并注册到 TestCase 中。这样执行该 TestCase 时,虽然只编码了一条单测,但是却有多条用例被执行。
整个过程都是对类和单测方法的元数据属性进行各种操作来实现的。