测试框架ddt拆解
前言
这段时间在找顺手的测试用例生成框架,主要需求是能够满足根据 程序注释或者参数的序列化器来反向生成一波测试用例即可,通过 注释跟序列化器的部分已经完成,就缺一个自动生成测试用例的部 分(测试专业的话叫参数化)。 刚好在这个帖子里提到了ddt+unitest,足够满足需要。加上ddt代 码足够精简,不妨拆解一下将技术用到别的地方。
样例分析
在官方给出的样例中,有如下段落:
@ddt
class FooTestCase(unittest.TestCase):
def test_undecorated(self):
self.assertTrue(larger_than_two(24))
@data(3, 4)
def test_larger_than_two(self, value):
self.assertTrue(larger_than_two(value))
这里提供了两个方法: 一个类装饰器ddt与方法装饰器data,就是这两个方法达到了类似这样的效果:
class FooTestCase(unittest.TestCase):
def test_undecorated(self):
self.assertTrue(larger_than_two(24))
def test_larger_than_two_3(self):
self.assertTrue(larger_than_two(3))
def test_larger_than_two_4(self):
self.assertTrue(larger_than_two(4))
猜测:
- ddt装饰器对于类实例化的时候做了处理,动态绑定上了由data传入的数据的对应方法。
- data装饰器对于被装饰函数做了标记绑定数据,并在实例化过程中跳过被装饰方法。
源码拆解
先看data部分:
def data(*values):
"""
Method decorator to add to your test methods.
Should be added to methods of instances of ``unittest.TestCase``.
"""
global index_len
index_len = len(str(len(values)))
return idata(values)
def idata(iterable):
"""
Method decorator to add to your test methods.
Should be added to methods of instances of ``unittest.TestCase``.
"""
def wrapper(func):
setattr(func, DATA_ATTR, iterable)
return func
return wrapper
这里仅仅采用了setattr对func方法进行绑定到func.DATA_ATTR下。
然后是ddt部分:
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):
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(
name,
getattr(v, "__name__", v),
i,
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
这里的类装饰器使用了cls作为类实例对象,然后遍历实力对象里的__dict__属性。
参考 built-in types — python 3.9.7 documentation中得知 在类属性的__dict__中,包含静态函数、类函数等属性。
而在这个过程中,我们看到这里遍历了每个函数是否存在有DATA_ATTR标记。 若有数据标记,则使用DATA_ATTR中装饰上的数据动态生成一个新函数,然后将新函数 绑定类属性里,完成过程后删除原函数。
def feed_data(func, new_name, test_data_docstring, *args, **kwargs):
"""
This internal method decorator feeds the test data item to the test.
"""
@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):
# Maybe the user has added some of the formating strings
# unintentionally in the docstring. Do not raise an exception
# as it could be that user is not aware of the
# formating feature.
pass
return wrapper
而实际上,这里主要做了一个返回被装饰的函数传参为data所传数据的结果,其余的是为了保证 生成的函数对于__doc__,name,__wrapped__等等属性的一致性。
总结
这里使用了python里的OOP特性,对于函数也是一个object,对函数的object装饰写入属性作为标记, 然后装饰类属性,通过类的__dict__属性来获取被装饰后函数是否存在,然后执行动态的 函数绑定逻辑。使得框架在对于类实例化后,或者相应的参数化测试用例。
技术点
- 运行时针对对于类属性__dict__的访问
- 运行时对于类方法的动态绑定技巧
疑问点
- 这里的方法绑定过程,是否达到了跟functool.warps相等的效果,不丢失原函数信息?
- 疑问点,这里的绑定函数,是否会有出现函数名冲突的情况,如何处理?