接口自动化总结
写下本文的目的
对半年多来的接口自动化测试工作做一个总结,同时也希望能够有志同道合的朋友一起交流,本文中仅贴出了接口框架里面部
分代码,更多的是希望有朋友一起讨论下接口框架的实现思路、应有的功能、可维护性等。
基础思路
每条Case继承与TestCase基类,TestCase基类中,实现了每条Case工作流程:
InitTest(初始化,一般没用到) à PreTest(可以理解为前置条件,例如数据库操作产生数据)
à RunTest(测试执行时的具体逻辑,只包含逻辑,不包含代码,代码在TestModule中封装好了)à PostTest(清理数据,确保单条Case可以重复执行)
TestModule:封装每个接口的模板类,每个接口对应于一个方法,参数由定参、默认参数、变参组成,每条Case调用TestModule中封装好的方法,根据变参组合,设计出不同的Case。
TestModule继承于Common公共类,Common中封装了requests的http请求
TestModule中一个退出登录接口方法的封装例子:
deflogout(self,UserID,SessionID): #UserID SessionID为变参数
'''退出登录接口
'''
path = '/Users/Logout.ashx' #接口路径
requestData = {
"UserID":UserID,
"SessionID":SessionID
}
requestData = dict(requestData,**self.commonparam_adr)
# self.commonparam_adr为公共参数,即定参
resJson = self.httpPost(requestData,self.module,path)
# print resJson
returnresJson
具体的一个Case:
class logout(TestCase):
'''【退出登录】退出登录
'''
owner = 'yuziqiang'
timeout = 5
def runTest(self):
global m
m = TestModule()
#登录
res1 = m.login(User = 10086, pwd = ***)
#注销传入的UserID、SessionID为登录返回的UserID、SessionID
UserID = res1['UserID']
SessionID = res1['SessionID']
#注销
resJson = m.logout(UserID, SessionID = SessionID)
self.assertEqual('error_code', resJson['error_code'], '0') #对接口返回的标志性参数断言,例如此处的接口反馈码
可以看出每个Case里面基本上只有业务逻辑
测试用例批量运行
TestRunner:
负责载入与筛选测试用例集,对筛选后的用例起线程运行。
熟悉UnitTest+HtmlRunner框架的,可以讲本文中的TestRunner理解为UnitTest中的TestLoader类
筛选准则:
单个测试用例类是否继承于TestCase基类,是否有owner子段,是否有Runtest方法
defloadTestsFromClass(self, testclass):
"""返回测试用例list"""
testclasses = []
if isinstance(testclass, types.TypeType)and \
issubclass(testclass, TestCase)and \
hasattr(testclass,"runTest"): #判断这个类是否满足正常的要求,满足就将该条用例的类名称添加到列表中
testclasses.append(testclass)
return testclasses
TestReport:
每条Case执行后,都会产生xml日志,TestReport和TestRunner会处理这些xml日志,生成Html文档,将Html报告通过邮箱的形式发送到相关负责人
# -*- coding: utf-8 -*-
"""
创建测试报告并以QQ邮箱的形式发送
"""
importsocket
importsys, os, smtplib
importurllib
importwin32com.client
fromemail.MIMETextimportMIMEText
fromemail.headerimportHeader
classEnumEmailPriority(object):
"""邮件优先级
"""
Low = 'Low'
Normal = 'Normal'
High = 'Hight'
defsendemail(htmldoc, recipents, title,priority=EnumEmailPriority.Normal):
"""发送邮件
@typehtmldoc: string
@param htmldoc:邮件内容,必须为utf8编码的字符串
@typerecipents: string
@param recipents:邮件接收人,若存在多个接收人,请使用分号";"隔开。
@type title: string
@param title:邮件标题,必须为utf8编码的字符串
"""
usr_from='*******@qq.com'
pwd=******
htmldoc = MIMEText(htmldoc,"html")
htmldoc.set_charset("gb2312")
htmldoc["Subject"] = Header(title,'utf-8').encode()
ifisinstance(recipents, list):
htmldoc["To"] =";".join(recipents) #htmldoc['To']接收的是字符串而不是list,如果有多个邮件地址,用;分隔即可
else:
htmldoc["To"] = recipents
htmldoc["From"] = usr_from
smtp_server = 'smtp.qq.com'
server = smtplib.SMTP_SSL(smtp_server,465)
server.login(usr_from, pwd)
importstring
recipents = string.splitfields(recipents,",")
# print recipents ,type(recipents)
server.sendmail(usr_from, recipents, htmldoc.as_string())
server.quit()
defprint_usage():
USAGE=\
u"""
用法:%(Program)s测试结果目录 邮件标题 邮件帐号
测试结果目录:存放用例运行结果的目录,就是包含TestResults.xml的目录
邮件帐号:测试报告需要发送的邮件帐号,以逗号隔开。
"""
print USAGE % {"Program":os.path.basename(sys.argv[0])} #获取当前py文件名,即testreport.py
if__name__ =='__main__':
if len(sys.argv) !=4:
print_usage()
sys.exit(0) #正常退出
rstdir = sys.argv[1]
# mail_title = sys.argv[2].decode('gbk').encode('utf8')
mail_title = sys.argv[2]
recipents = sys.argv[3].split(',')
rstdir = rstdir.decode('gbk')
xmlrst = os.path.join(rstdir, "RunResults.xml")
xsltfile = os.path.join(rstdir, "RunResult.xsl")
#将TestResult和Module的log属性设为绝对路径
importxml.dom.minidomasdom
xmlstream = open(xmlrst)
xmldata = xmlstream.read()
xmlstream.close()
xmldom = dom.parseString(xmldata)
rstnodes = xmldom.getElementsByTagName('TestResult')
for nodein rstnodes:
log = node.attributes['log'].nodeValue
dirname = rstdir.split('\\')
pcname = socket.gethostname()
#获取本机电脑名
hostname =socket.getfqdn(socket.gethostname())
#获取本机ip
hostip = socket.gethostbyname(hostname)
log = 'ftp://' + str(hostip) +'/result/' + dirname[len(dirname)-1] +'/' + log
#node.attributes['log'].nodeValue = os.path.join(rstdir, log)
node.attributes['log'].nodeValue = log
rstnodes = xmldom.getElementsByTagName('Module')
for nodein rstnodes:
log = node.attributes['log'].nodeValue
node.attributes['log'].nodeValue =os.path.join(rstdir, log)
#将xml转换为html
xmlsource = win32com.client.Dispatch('MSXML2.DOMDocument')
xmlsource.loadXML(xmldom.toxml())
stylesheet = win32com.client.Dispatch('MSXML2.DOMDocument')
stylesheet.load(xsltfile)
htmldoc = xmlsource.transformNode(stylesheet)
htmldoc = htmldoc.encode('gbk')
htmldoc = urllib.unquote(htmldoc)
sendemail(htmldoc,','.join(recipents),mail_title)
生成的Html报告
用例Log:
ftp链接访问
TestExcute
传入TestRunner以及TestReport中的初始数据,例如指定testrunner中运行哪个模块包下的用例、执行何种优先级的用例;指定TestReport中目标收件人、邮件标题等
附:xml日志转换为xls表格:
xmlIntoXls
xmlIntoXls从xml日志中提取出需要用到的数据,例如,用例描述、负责人。优先级、测试结果等,生成xls测试结果报告。
xml日志如下:
xmlIntoxls代码如下:
#-*- coding: utf-8 -*-
"""将XML文档的测试结果对应转换为result.xls"""
importos
importxml.etree.ElementTreeas ET
fromxlwtimport*
importxlwt
from_ctypesimportalignment
classxml_into_xls():
def__init__(self):
self.keyword = ['测试用例','用例描述','负责人','状态','耗时','测试结果'] #每列字样
self.style = XFStyle()
self.datatable=xlwt.Workbook(encoding='utf-8')
self.newsheet = self.datatable.add_sheet('mxxx',cell_overwrite_ok=True)
#新建excel文档sheet 第二个参数允许覆盖
self.newsheet.col(0).width =18000 #设置单元格第0列宽度
self.newsheet.col(1).width =15000
self.newsheet.col(2).width =3000
self.newsheet.col(3).width =3000
self.newsheet.col(4).width =3000
self.newsheet.col(5).width =3000
defgetTrEle(self):
"""获取xml中"TestResult"子目录对应的元素列表
"""
xmlrst =os.path.join(os.path.dirname(__file__),"RunResults.xml")
tree=ET.parse(xmlrst)
root=tree.getroot() #获取根目录
# print root
testsuite_per=root.findall("TestResult") #获取根目录下的子目录
# print testsuite_per
return testsuite_per
defsetstyle(self,style_num):
""""style_num参数为整形
style_num=0 :标题风格
style_num=1 :结果正确时奇数行风格
style_num=2 :结果正确时偶数行风格
style_num=3 :结果错误时奇数行风格
style_num=4 :结果错误时偶数行风格
"""
if style_num ==0:
pattern = Pattern() #创建一个模式
pattern.pattern =Pattern.SOLID_PATTERN #设置其模式为实型
pattern.pattern_fore_colour =7 #常用颜色可设置为22,26,7,42
#设置单元格背景颜色 0 = Black, 1 = White,2 = Red, 3 = Green, 4 = Blue, 5 = Yellow, 6 =Magenta,
self.style.pattern =pattern #将赋值好的模式参数导入Style
#创建一个Line_data列表,并将其值赋为测试表,以utf-8编码时中文前加u
fnt = Font() #创建一个文本格式,包括字体、字号和颜色样式特性
fnt.name = u'微软雅黑' #设置其字体为微软雅黑
fnt.colour_index = 0 #设置其字体颜色 0:黑 1:白 2:红 5:黄
fnt.height = 0x00e8
fnt.bold = True
self.style.font = fnt
alignment = Alignment()
alignment.horz = 0x02 #0:常规 1:水平左对齐 2:水平居中
self.style.alignment =alignment
borders = Borders() #设置下框线样式
borders.left = 1 # 1为常规框线
borders.right = 1
borders.top = 1
borders.bottom = 1
self.style.borders = borders
returnself.style
elif style_num ==1:
pattern = Pattern() #创建一个模式
pattern.pattern =Pattern.SOLID_PATTERN #设置其模式为实型
pattern.pattern_fore_colour =42 #常用颜色可设置为22,26,7,42
#设置单元格背景颜色 0 = Black, 1 = White,2 = Red, 3 = Green, 4 = Blue, 5 = Yellow, 6 =Magenta, the list goes on...
self.style.pattern =pattern
self.style.font=xlwt.Font() #恢复默认值
self.style.alignment=xlwt.Alignment()
returnself.style
elif style_num ==2:
self.style.pattern=xlwt.Pattern()
self.style.font=xlwt.Font() #恢复默认值
self.style.alignment=xlwt.Alignment()
returnself.style
elif style_num ==3:
pattern = Pattern() #创建一个模式
pattern.pattern =Pattern.SOLID_PATTERN #设置其模式为实型
pattern.pattern_fore_colour =42 #常用颜色可设置为22,26,7,42
#设置单元格背景颜色 0 = Black, 1 = White,2 = Red, 3 = Green, 4 = Blue, 5 = Yellow, 6 =Magenta, the list goes on...
self.style.pattern = pattern
fnt = Font() #创建一个文本格式,包括字体、字号和颜色样式特性
# fnt.name = u'微软雅黑' #设置其字体为微软雅黑
fnt.colour_index = 2 #设置其字体颜色 0:黑 1:白 2:红 5:黄
fnt.height = 0x00c8
fnt.bold = True
self.style.font = fnt
# self.style.font=xlwt.Font() #恢复默认值
self.style.alignment=xlwt.Alignment() #恢复对其格式的默认值
returnself.style
elif style_num ==4:
self.style.pattern=xlwt.Pattern()
self.style.font=xlwt.Font() #恢复默认值
self.style.alignment=xlwt.Alignment()
fnt = Font() #创建一个文本格式,包括字体、字号和颜色样式特性
# fnt.name = u'微软雅黑' #设置其字体为微软雅黑
fnt.colour_index = 2 #设置其字体颜色 0:黑 1:白 2:红 5:黄
fnt.height = 0x00c8
fnt.bold = True
self.style.font = fnt
# self.style.font=xlwt.Font() #恢复默认值
returnself.style
defwriteBody(self,row,num_style):
'''写入第row行的测试用例结果
'''
testsuite_per = self.getTrEle()
self.newsheet.write(row+1,0,testsuite_per[row].get('name'),self.setstyle(num_style))
self.newsheet.write(row+1,1,testsuite_per[row].get('casemark'),self.setstyle(num_style))
self.newsheet.write(row+1,2,testsuite_per[row].get('owner'),self.setstyle(num_style))
self.newsheet.write(row+1,3,testsuite_per[row].get('status'),self.setstyle(num_style))
self.newsheet.write(row+1,4,testsuite_per[row].get('duration'),self.setstyle(num_style))
self.newsheet.write(row+1,5, testsuite_per[row].get('result'),self.setstyle(num_style))
defwritExcel(self):
'''写入标题、测试用例Body结果
'''
testsuite_per = self.getTrEle() #从Runresult.xml中获取每条用例xml列表
for iin range(len(self.keyword)): #写入标题
self.newsheet.write(0, i, self.keyword[i],self.setstyle(0))
for row_resultinrange(0,len(testsuite_per)):
if row_result %2==0: #偶数行风格
if testsuite_per[row_result].get('result') =="True":
self.writeBody(row_result,1)
else:
self.writeBody(row_result,3)
elif row_result %2==1: #奇数行风格
if testsuite_per[row_result].get('result') =="True":
self.writeBody(row_result,2)
else:
self.writeBody(row_result,4)
defrun(self):
self.writExcel()
self.datatable.save('result.xls') #保存结果名字为result.xls
if__name__ =='__main__':
turn = xml_into_xls()
turn.run()
总结
框架中的测试数据都直接写在代码里面了,后续考虑提取出用例里面使用的基础数据,例如账号、api地址、公共参数、邮箱收件人等,计划使用yaml存储这些数据,方便维护。
后期会不定期更新博文,包括但不限于接口脚本。