时隔已久,再次冒烟,自动化测试工作仍在继续,自动化测试中的数据驱动技术尤为重要,不然咋去实现数据分离呢,对吧,这里就简单介绍下与传统unittest自动化测试框架匹配的DDT数据驱动技术。
话不多说,先撸一波源码,其实整体代码并不多
#-*-coding:utf-8-*-
#ThisfileisapartofDDT(https://github.com/txels/ddt)
#Copyright2012-2015CarlesBarrobésandDDTcontributors
#Fortheexactcontributionhistory,seethegitrevisionlog.
#DDTislicensedundertheMITLicense,includedin
#https://github.com/txels/ddt/blob/master/LICENSE.md
importinspect
importjson
importos
importre
importcodecs
fromfunctoolsimportwraps
try:
importyaml
exceptImportError:#pragma:nocover
_have_yaml=False
else:
_have_yaml=True
__version__='1.2.1'
#Theseattributeswillnotconflictwithanyrealpythonattribute
#Theyareaddedtothedecoratedtestmethodandprocessedlater
#bythe`ddt`classdecorator.
DATA_ATTR='%values'#storethedatathetestmustrunwith
FILE_ATTR='%file_path'#storethepathtoJSONfile
UNPACK_ATTR='%unpack'#rememberthatwehavetounpackvalues
index_len=5#defaultmaxlengthofcaseindex
try:
trivial_types=(type(None),bool,int,float,basestring)
exceptNameError:
trivial_types=(type(None),bool,int,float,str)
defis_trivial(value):
ifisinstance(value,trivial_types):
returnTrue
elifisinstance(value,(list,tuple)):
returnall(map(is_trivial,value))
returnFalse
defunpack(func):
"""
Methoddecoratortoaddunpackfeature.
"""
setattr(func,UNPACK_ATTR,True)
returnfunc
defdata(*values):
"""
Methoddecoratortoaddtoyourtestmethods.
Shouldbeaddedtomethodsofinstancesof``unittest.TestCase``.
"""
globalindex_len
index_len=len(str(len(values)))
returnidata(values)
defidata(iterable):
"""
Methoddecoratortoaddtoyourtestmethods.
Shouldbeaddedtomethodsofinstancesof``unittest.TestCase``.
"""
defwrapper(func):
setattr(func,DATA_ATTR,iterable)
returnfunc
returnwrapper
deffile_data(value):
"""
Methoddecoratortoaddtoyourtestmethods.
Shouldbeaddedtomethodsofinstancesof``unittest.TestCase``.
``value``shouldbeapathrelativetothedirectoryofthefile
containingthedecorated``unittest.TestCase``.Thefile
shouldcontainJSONencodeddata,thatcaneitherbealistora
dict.
Incaseofalist,eachvalueinthelistwillcorrespondtoone
testcase,andthevaluewillbeconcatenatedtothetestmethod
name.
Incaseofadict,keyswillbeusedassuffixestothenameofthe
testcase,andvalueswillbefedastestdata.
"""
defwrapper(func):
setattr(func,FILE_ATTR,value)
returnfunc
returnwrapper
defmk_test_name(name,value,index=0):
"""
Generateanewnameforatestcase.
Itwilltaketheoriginaltestnameandappendanordinalindexanda
stringrepresentationofthevalue,andconverttheresultintoavalid
pythonidentifierbyreplacingextraneouscharacterswith``_``.
Weavoiddoingstr(value)ifdealingwithnon-trivialvalues.
Theproblemispossibledifferentnameswithdifferentruns,e.g.
differentorderofdictionarykeys(seePYTHONHASHSEED)ordealing
withmockobjects.
Trivialscalarvaluesarepassedasis.
A"trivial"valueisaplainscalar,oratupleorlistconsisting
onlyoftrivialvalues.
"""
#Addzerosbeforeindextokeeporder
index="{0:0{1}}".format(index+1,index_len)
ifnotis_trivial(value):
return"{0}_{1}".format(name,index)
try:
value=str(value)
exceptUnicodeEncodeError:
#fallbackforpython2
value=value.encode('ascii','backslashreplace')
test_name="{0}_{1}_{2}".format(name,index,value)
returnre.sub(r'\W|^(?=\d)','_',test_name)
deffeed_data(func,new_name,test_data_docstring,*args,**kwargs):
"""
Thisinternalmethoddecoratorfeedsthetestdataitemtothetest.
"""
@wraps(func)
defwrapper(self):
returnfunc(self,*args,**kwargs)
wrapper.__name__=new_name
wrapper.__wrapped__=func
#setdocstringifexists
iftest_data_docstringisnotNone:
wrapper.__doc__=test_data_docstring
else:
#Trytocallformatonthedocstring
iffunc.__doc__:
try:
wrapper.__doc__=func.__doc__.format(*args,**kwargs)
except(IndexError,KeyError):
#Maybetheuserhasaddedsomeoftheformatingstrings
#unintentionallyinthedocstring.Donotraiseanexception
#asitcouldbethatuserisnotawareofthe
#formatingfeature.
pass
returnwrapper
defadd_test(cls,test_name,test_docstring,func,*args,**kwargs):
"""
Addatestcasetothisclass.
Thetestwillbebasedonanexistingfunctionbutwillgiveitanew
name.
"""
setattr(cls,test_name,feed_data(func,test_name,test_docstring,
*args,**kwargs))
defprocess_file_data(cls,name,func,file_attr):
"""
Processtheparameterinthe`file_data`decorator.
"""
cls_path=os.path.abspath(inspect.getsourcefile(cls))
data_file_path=os.path.join(os.path.dirname(cls_path),file_attr)
defcreate_error_func(message):#pylint:disable-msg=W0613
deffunc(*args):
raiseValueError(message%file_attr)
returnfunc
#Iffiledoesnotexist,provideanerrorfunctioninstead
ifnotos.path.exists(data_file_path):
test_name=mk_test_name(name,"error")
test_docstring="""Error!"""
add_test(cls,test_name,test_docstring,
create_error_func("%sdoesnotexist"),None)
return
_is_yaml_file=data_file_path.endswith((".yml",".yaml"))
#Don'thaveYAMLbutwanttouseYAMLfile.
if_is_yaml_fileandnot_have_yaml:
test_name=mk_test_name(name,"error")
test_docstring="""Error!"""
add_test(
cls,
test_name,
test_docstring,
create_error_func("%sisaYAMLfile,pleaseinstallPyYAML"),
None
)
return
withcodecs.open(data_file_path,'r','utf-8')asf:
#LoadthedatafromYAMLorJSON
if_is_yaml_file:
data=yaml.safe_load(f)
else:
data=json.load(f)
_add_tests_from_data(cls,name,func,data)
def_add_tests_from_data(cls,name,func,data):
"""
Addtestsfromdataloadedfromthedatafileintotheclass
"""
fori,eleminenumerate(data):
ifisinstance(data,dict):
key,value=elem,data[elem]
test_name=mk_test_name(name,key,i)
elifisinstance(data,list):
value=elem
test_name=mk_test_name(name,value,i)
ifisinstance(value,dict):
add_test(cls,test_name,test_name,func,**value)
else:
add_test(cls,test_name,test_name,func,value)
def_is_primitive(obj):
"""Findsoutiftheobjisa"primitive".Itissomewhathackybutitworks.
"""
returnnothasattr(obj,'__dict__')
def_get_test_data_docstring(func,value):
"""Returnsadocstringbasedonthefollowingresolutionstrategy:
1.Passedvalueisnota"primitive"andhasadocstring,thenuseit.
2.InallothercasesreturnNone,i.ethetestnameisused.
"""
ifnot_is_primitive(value)andvalue.__doc__:
returnvalue.__doc__
else:
returnNone
defddt(cls):
"""
Classdecoratorforsubclassesof``unittest.TestCase``.
Applythisdecoratortothetestcaseclass,andthen
decoratetestmethodswith``@data``.
Foreachmethoddecoratedwith``@data``,thiswilleffectivelycreateas
manymethodsasdataitemsarepassedasparametersto``@data``.
Thenamesofthetestmethodsfollowthepattern
``original_test_name_{ordinal}_{data}``.``ordinal``isthepositionofthe
dataargument,startingwith1.
Fordataweuseastringrepresentationofthedatavalueconvertedintoa
validpythonidentifier.If``data.__name__``exists,weusethatinstead.
Foreachmethoddecoratedwith``@file_data('test_data.json')``,the
decoratorwilltrytoloadthetest_data.jsonfilelocatedrelative
tothepythonfilecontainingthemethodthatisdecorated.Itwill,
foreach``test_name``keycreateasmanymethodsinthelistofvalues
fromthe``data``key.
"""
forname,funcinlist(cls.__dict__.items()):
ifhasattr(func,DATA_ATTR):
fori,vinenumerate(getattr(func,DATA_ATTR)):
test_name=mk_test_name(name,getattr(v,"__name__",v),i)
test_data_docstring=_get_test_data_docstring(func,v)
ifhasattr(func,UNPACK_ATTR):
ifisinstance(v,tuple)orisinstance(v,list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
else:
#unpackdictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
add_test(cls,test_name,test_data_docstring,func,v)
delattr(cls,name)
elifhasattr(func,FILE_ATTR):
file_attr=getattr(func,FILE_ATTR)
process_file_data(cls,name,func,file_attr)
delattr(cls,name)
returncls
ddt源码
通过源码的说明,基本可以了解个大概了,其核心用法就是利用装饰器来实现功能的复用及扩展延续,以此来实现数据驱动,现在简单介绍下其主要函数的基本使用场景。
1.@ddt(cls),其服务于unittest类装饰器,主要功能是判断该类中是否具有相应ddt装饰的方法,如有则利用自省机制,实现测试用例命名mk_test_name、数据回填_add_tests_from_data并通过add_test添加至unittest的容器TestSuite中去,然后执行得到testResult,流程非常清晰。
defddt(cls):
forname,funcinlist(cls.__dict__.items()):
ifhasattr(func,DATA_ATTR):
fori,vinenumerate(getattr(func,DATA_ATTR)):
test_name=mk_test_name(name,getattr(v,"__name__",v),i)
test_data_docstring=_get_test_data_docstring(func,v)
ifhasattr(func,UNPACK_ATTR):
ifisinstance(v,tuple)orisinstance(v,list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
else:
#unpackdictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
add_test(cls,test_name,test_data_docstring,func,v)
delattr(cls,name)
elifhasattr(func,FILE_ATTR):
file_attr=getattr(func,FILE_ATTR)
process_file_data(cls,name,func,file_attr)
delattr(cls,name)
returncls
2.@file_data(PATH),其主要是通过process_file_data方法实现数据解析,这里通过_add_tests_from_data实现测试数据回填,通过源码可以得知目前文件只支持Yaml和JSON数据文件,想扩展其它文件比如xml等直接改源码就行
defprocess_file_data(cls,name,func,file_attr):
"""
Processtheparameterinthe`file_data`decorator.
"""
cls_path=os.path.abspath(inspect.getsourcefile(cls))
data_file_path=os.path.join(os.path.dirname(cls_path),file_attr)
defcreate_error_func(message):#pylint:disable-msg=W0613
deffunc(*args):
raiseValueError(message%file_attr)
returnfunc
#Iffiledoesnotexist,provideanerrorfunctioninstead
ifnotos.path.exists(data_file_path):
test_name=mk_test_name(name,"error")
test_docstring="""Error!"""
add_test(cls,test_name,test_docstring,
create_error_func("%sdoesnotexist"),None)
return
_is_yaml_file=data_file_path.endswith((".yml",".yaml"))
#Don'thaveYAMLbutwanttouseYAMLfile.
if_is_yaml_fileandnot_have_yaml:
test_name=mk_test_name(name,"error")
test_docstring="""Error!"""
add_test(
cls,
test_name,
test_docstring,
create_error_func("%sisaYAMLfile,pleaseinstallPyYAML"),
None
)
return
withcodecs.open(data_file_path,'r','utf-8')asf:
#LoadthedatafromYAMLorJSON
if_is_yaml_file:
data=yaml.safe_load(f)
else:
data=json.load(f)
_add_tests_from_data(cls,name,func,data)
3.@date(*value),简单粗暴的直观实现数据驱动,直接将可迭代对象传参,进行数据传递,数据之间用逗号“,”隔离,代表一组数据,此时如果实现unpack,则更加细化的实现数据驱动,切记每组数据对应相应的形参。
defunpack(func):
"""
Methoddecoratortoaddunpackfeature.
"""
setattr(func,UNPACK_ATTR,True)
returnfunc
defdata(*values):
"""
Methoddecoratortoaddtoyourtestmethods.
Shouldbeaddedtomethodsofinstancesof``unittest.TestCase``.
"""
globalindex_len
index_len=len(str(len(values)))
returnidata(values)
defidata(iterable):
"""
Methoddecoratortoaddtoyourtestmethods.
Shouldbeaddedtomethodsofinstancesof``unittest.TestCase``.
"""
defwrapper(func):
setattr(func,DATA_ATTR,iterable)
returnfunc
returnwrapper
4.实例
#-*-coding:utf-8-*-
__author__='暮辞'
importtime,random
fromddtimportddt,data,file_data,unpack
importunittest
importjson
fromHTMLTestRunnerimportHTMLTestRunner
@ddt
classDemo(unittest.TestCase):
@file_data("./migrations/test.json")
deftest_hello(self,a,**b):
'''
测试hello
'''
printa
printb
#print"hello",a,type(a)
ifisinstance(a,list):
self.assertTrue(True,"2")
else:
self.assertTrue(True,"3")
@data([1,2,3,4])
deftest_world(self,*b):
'''
测试world
'''
printb
self.assertTrue(True)
@data({"test1":[1,2],"test2":[3,4]},{"test1":[1,2],"test2":[3,4]})
@unpack
deftest_unpack(self,**a):
'''
测试unpack
'''
printa
self.assertTrue(True)
if__name__=="__main__":
suit=unittest.TestSuite()
test=unittest.TestLoader().loadTestsFromTestCase(Demo)
suit.addTests(test)
#suit.addTests(test)
withopen("./migrations/Demo.html","w")asf:
result=HTMLTestRunner(stream=f,description=u"Demo测试报告",title=u"Demo测试报告")
result.run(suit)
测试结果:
至此关于ddt的数据驱动暂时告一段落了,后面还会介绍基于excel、sql等相关的数据驱动内容,并进行对比总结,拭目以待~
如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h54871.shtml