ddt源码分析

2023-03-06 19:54:08 浏览数 (2)

# 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 填写多个测试的参数,这样执行就完成了参数化的单测了。

代码语言:javascript复制
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_1test_larger_than_two_2_2,名称的规则是:单测的名称_索引_参数

代码语言:javascript复制
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 用来保存参数化的长度。
代码语言:javascript复制
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 时,虽然只编码了一条单测,但是却有多条用例被执行。

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

0 人点赞