前言
python做自动化测试,大多数都应该使用过ddt这个模块,它可以自动根据用例数据,来生成测试用例,能够很方便的将测试数据和测试用例执行的逻辑进行分离。接下来和大家,手把手撸出一个ddt。
DDT的实现原理
首先我们来看一下ddt的基本使用:
@ddt
class TestLogin(unittest.TestCase):
@data(*Cases)
def test_login__pass(self,item):
ddt在使用时非常简洁,也就是两个装饰器,@ddt这个装饰器装饰测试类,@data这个装饰器装饰器用例方法并传入测试数据。
这两个装饰器实现的效果就是根据传入的用例数据自动生成用例。具体是怎么实现的呢?
其实实现的思路也特别的简单,也就两个步骤:
第一步:把传进来的用例数据保存起来
第二步:遍历用例数据,每遍历一条数据 就动态的给测试类添加一个用例方法。
ddt中的两个装饰器其实实现的就是这么两个步骤:
@data:做的是第一步将传入测试数据保存起来;
@ddt做的是第二步,遍历用例数据,给测试类动态添加用例方法。
data装饰器的实现
前面我们说到data这个装饰器,做的事情是将用例数据保存起来。那么如何保存呢?其实最简单的方式就是 保存被装饰的这个用例方法的属性。接下来我们来具体实现:
先看一个ddt使用的案例
@ddt
class TestLogin(unittest.TestCase):
@data(11,22)
def test_login(self, item):
pass
了解过装饰器装饰器原理的小伙伴,应该都知道上面@data(11,22) 这行代码执行的效果等同于
test_login = data(11,22)(test_login)
接下来我们来分析一下上面这行代码,首先是调用data这个装饰器函数,把用例数据11,22当成参数传入进去,然后返回一个可调用对象(函数),再次调用返回的函数并把用例方法传入进去。明确了调用的流程那么我们就可以结合之前的需求去定义data这个装饰器函数了。
具体实现如下:
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
代码解读:
前面的案例在使用data时,执行的test_login = data(11,22)(test_login)
先调用data传入的11,22 通过不定长参数args接收,然后返回嵌套的函数wrapper
然后调用返回的wrapper函数,传入被装饰的test_login方法
在wrapper函数中我们把用例数据保存为test_login这个方法的PARAMS属性,再把test_login返回,到此为止,data这个装饰器我们就实现用例数据的保存
ddt装饰器的实现
通过data这个装饰器我们实现了用例数据保存之后,我们接下来实现ddt这个装饰器,根据用例数据生成测试用例。前面的案例 @ddt装饰测试类的时候,实际上执行的效果等同于下面的代码
TestLogin = ddt(TestLogin)
这行代码就是把被装饰器的类传入到ddt这个装饰器函数中,再把返回值赋值给TestLogin。之前我们分析的时候说了ddt这个装饰器做的事情是遍历用例数据,动态的给测试类添加用例方法,接下来我们就来实现ddt这个装饰器内部的逻辑。
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
new_test_name ="{}_{}".format(name,index)
setattr(cls, new_test_name, func)
else:
delattr(cls, name)
return cls
代码解读:
ddt函数内部逻辑说明:
调用ddt这个函数时会把测试类当成参数传入进来;
然后通过cls.__dict__获取测试的所有属性和方法,进行遍历;
判断遍历出来的属性或方法 有没有PARAMS这个属性;
如果有,则说明这个方法用data装饰器装饰过并传入了用例数据;
通过getattr(func, “PARAMS”)获取所有的用例数据,进行遍历;
每遍历出来一组用例数据,生产一个用例方法名, 再动态的给测试类添加一个用例方法;
遍历完所有用例数据之后,删除测试类原来定义的测试方法;
最后返回测试类;
当目前为止ddt和data这两个装饰器函数的基本功能实现了,可以自动根据用例数据生成测试用例了,接下来我们写个测试类来检查一下
# 定义装饰器函数data
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
# 定义装饰器函数ddt
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
new_test_name = "{}_{}".format(name, index)
setattr(cls, new_test_name, func)
else:
delattr(cls, name)
return cls
import unittest
# 编写测试类
@ddt
class TestDome(unittest.TestCase):
@data(11, 22, 33, 44)
def test_demo(self):
pass
运行上述用例,我们就会发现执行了四条用例,根据用例数据生成用例的功能就已经实现了。
用例参数传递的问题
虽然上面基本的功能已经实现了,但是还存在一个问题。用例的数据没有传递到用例方法中。那么用例数据传递怎么实现了,我们可以通过一个闭包函数对用例方法进行修,从而实现在调用用例方法的时候,把用例测试当成参数传递进去。修改原有用例方法的函数代码如下
from functools import wraps
def update_test_func(test_func,case_data):
@wraps(test_func)
def wrapper(self):
return test_func(self, case_data)
return wrapper
代码解读:
上面我们定义了一个叫做update_test_func的闭包函数
闭包函数接收两个参数:test_func(接收用例方法),case_data(接收用例数据)
闭包函数返回一个嵌套函数,嵌套函数内部调用原来的用例方法,并传入测试数据
嵌套函数在定义时,使用了functools模块中的装饰器wraps来装饰,它可以让wrapper这个嵌套函数具有test_func这个用例函数的相关属性。
下面我们回到前面写的ddt这个函数中,在给测试类添加用例之前,调用update_test_func方法对用例方法进行修改。
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
# 生成一个用例方法名
new_test_name = "{}_{}".format(name, index)
# 修改原有的测试方法,设置用例数据为测试方法的参数
test_func = update_test_func(func,case_data)
setattr(cls, new_test_name, test_func)
else:
delattr(cls, name)
return cls
通过加上这一步之后,我们在测试类中 动态给测试类添加的测试方法,其实指向的全部是update_test_func里面定义的wrapper函数,在执行测试用的时候实际上也是执行的wrapper函数,而在wrapper函数内部,我们调用了原来定义的测试方法,并将用例数据传入了进去,到此为止ddt的功能我们就完全实现了。
完整代码
from functools import wraps
import unittest
# --------ddt的实现--------
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
def update_test_func(test_func, case_data):
@wraps(test_func)
def wrapper(self):
return test_func(self, case_data)
return wrapper
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
# 生成一个用例方法名
new_test_name = "{}_{}".format(name, index)
# 修改原有的测试方法,设置用例数据为测试方法的参数
test_func = update_test_func(func, case_data)
setattr(cls, new_test_name, test_func)
else:
delattr(cls, name)
return cls
# --------测试用例编写--------
@ddt
class TestDome(unittest.TestCase):
@data(11, 22, 33, 44)
def test_demo(self, data):
assert data < 40
#---------用例执行-----------
unittest.main()
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
【软件测试技术交流(资料分享)】:320231853(备注C)http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=rS49sB1dBN6wjk4SbxAjX80YS65Zy8TH&authKey=tlP2KE7Sut5Dq7EvwkG55B%2B0sWc5WpLYbuRGFftTLHed0FB22lskhUs4Dnw6hQRP&noverify=0&group_code=320231853
生命不息,奋斗不止。每一份努力都不会被辜负,只要坚持不懈,终究会有回报。珍惜时间,追求梦想。不忘初心,砥砺前行。你的未来,由你掌握!
生命短暂,时间宝贵,我们无法预知未来会发生什么,但我们可以掌握当下。珍惜每一天,努力奋斗,让自己变得更加强大和优秀。坚定信念,执着追求,成功终将属于你!
只有不断地挑战自己,才能不断地超越自己。坚持追求梦想,勇敢前行,你就会发现奋斗的过程是如此美好而值得。相信自己,你一定可以做到!