前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ddt源码分析

ddt源码分析

作者头像
编程黑洞
发布2023-03-06 19:54:08
3300
发布2023-03-06 19:54:08
举报
文章被收录于专栏:编程黑洞编程黑洞

# 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:\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 源码分析流程

我们首先来看看 @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 时,虽然只编码了一条单测,但是却有多条用例被执行。

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 0. 前言
  • # 1. 源码分析
    • # 1.1 example
      • # 1.2 源码分析流程
      • # 2. 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档