惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Vulnerabilities – Threatpost
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
Forbes - Security
Forbes - Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
人人都是产品经理
人人都是产品经理
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
GbyAI
GbyAI
Last Week in AI
Last Week in AI
Hacker News: Ask HN
Hacker News: Ask HN
U
Unit 42
S
SegmentFault 最新的问题
月光博客
月光博客
Security Archives - TechRepublic
Security Archives - TechRepublic
Attack and Defense Labs
Attack and Defense Labs
S
Secure Thoughts
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
Arctic Wolf
Schneier on Security
Schneier on Security
C
CERT Recently Published Vulnerability Notes
I
Intezer
V
Visual Studio Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
Google Online Security Blog
Google Online Security Blog
N
Netflix TechBlog - Medium
I
InfoQ
T
Tor Project blog
腾讯CDC
T
Tenable Blog
Webroot Blog
Webroot Blog
Y
Y Combinator Blog
TaoSecurity Blog
TaoSecurity Blog
Google DeepMind News
Google DeepMind News
AI
AI
C
Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V2EX - 技术
V2EX - 技术
PCI Perspectives
PCI Perspectives
C
CXSECURITY Database RSS Feed - CXSecurity.com
G
GRAHAM CLULEY
S
Schneier on Security

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 手动实现docker容器bridge网络模型 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 redis之五种基本数据类型 go中如何处理error pod中将代码与运行环境分离 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment 容器的本质 docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
ddt源码分析
zhengwenfeng · 2022-10-24 · via 郑文峰的博客

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

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

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