• ddt源码分析


    0. 前言

    ddt 是 python 的第三方库,主要是解决使用 unittest 来写单测时可以支持参数化的配置,这个库的使用方法可以参考我之前写的使用ddt实现unittest的参数化测试。本文主要是讲自己在学习 ddt 库时所获。

    ddt 库的使用方法是用装饰器来实现的,可以参考 python装饰器使用方法

    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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们执行上面的单测会发现,虽然我们代码只写了一个用例,但是执行却是 3 个用例,成功了 1 个,失败了 2 个,并且输出了失败的用例的名称,test_larger_than_two_1_1test_larger_than_two_2_2 ,名称的规则是:单测的名称_索引_参数

    FF.
    ======================================================================
    FAIL: test_larger_than_two_1_1 (__main__.DemoTestCase)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "c:\crazyboy\code\ddt\ddt.py", line 220, in wrapper
        return func(self, *args, **kwargs)
      File "C:\CrazyBoy\workspace\demo\demo.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:\crazyboy\code\ddt\ddt.py", line 220, in wrapper
        return func(self, *args, **kwargs)
      File "C:\CrazyBoy\workspace\demo\demo.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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    这是如何实现的呢?

    1.2 源码分析流程

    我们首先来看看 @data 装饰器里面做了什么?

    def data(*values):  
        return idata(values)
    
    • 1
    • 2

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后我们再来看装饰器@ddt 中,传入的 cls 是被装饰的单测类,通过该类,找到上面使用@data 装饰器中添加的属性 DATA_ATTR 和对应的单测方法,其中的每条数据都是一个用例,通过遍历该属性中的参数值调用函数 mk_test_name 去构造每一条参数的用例名称。

    然后再调用 add_test 函数去生成对应的单测用例。

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    我们看看 add_test 做了什么?很简单,就是给单测的 TestCase 添加属性,以单测用例名称为名,feed_data 的返回值为值。

    feed_data 中,根据单个参数值和被@data 装饰的函数组成一个新的单测用例,并返回出去。

    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    也就是说,参数化的每个值都会生成一个用例方法并注册到被@ddt 装饰的 TestCase 类中。

    2. 总结

    主要流程是:通过 @data 装饰器将参数化注册到该单测用例方法的 DATA_ATTR 属性中,然后@ddt 装饰器遍历当前 TestCase 的所有包含 DATA_ATTR 属性的用例方法,再遍历其 DATA_ATTR 的参数值,把每条参数值都生成一条用例方法,并注册到 TestCase 中。这样执行该 TestCase 时,虽然只编码了一条单测,但是却有多条用例被执行。

    整个过程都是对类和单测方法的元数据属性进行各种操作来实现的。

    欢迎关注,互相学习,共同进步~

    我的个人博客

    我的微信公众号:编程黑洞

  • 相关阅读:
    python之阈值分割
    原生js实现移动端京东首页搜索框、菜单栏滑动弹回、倒计时、banner动画、缓慢返回顶部效果(HTML+CSS+JS)
    云原生可观测框架 OpenTelemetry 基础知识(架构/分布式追踪/指标/日志/采样/收集器)
    图数据挖掘(二):网络的常见度量属性
    Redis的性能管理
    【【萌新的SOC学习之小水文系列】】
    发现一款自学Java可视化工具神器,非常牛逼,太爱了!
    RTT外设驱动使用2--ADC串口添加
    新一代 Kaldi 中的 LSTM GradientFilter
    (附源码)mysql音乐播放器小程序 毕业设计 170900
  • 原文地址:https://blog.csdn.net/qq_22918243/article/details/127486041