前言

这段时间在找顺手的测试用例生成框架,主要需求是能够满足根据 程序注释或者参数的序列化器来反向生成一波测试用例即可,通过 注释跟序列化器的部分已经完成,就缺一个自动生成测试用例的部 分(测试专业的话叫参数化)。 刚好在这个帖子里提到了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))

猜测:

  1. ddt装饰器对于类实例化的时候做了处理,动态绑定上了由data传入的数据的对应方法。
  2. 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相等的效果,不丢失原函数信息?
  • 疑问点,这里的绑定函数,是否会有出现函数名冲突的情况,如何处理?

参考资料

Example usage — DDT 1.4.1 documentation

built-in types — python 3.9.7 documentation