空余时间写了一个接口自动化测试框架,为后期项目自动化测试规划奠定了一些基础的东西,顺便记录一下整个接口自动化框架设计过程中的一些心得和体会,写这篇文章的目的旨在分享和交流,自动化小白读完这篇文章,应该能够对接口自动化有一个很好的轮廓和理解
OK,首先说下这次框架设计的基础原理,框架主要采用的是python+excel+request+unittest方式,主要包含以下流程
1、excel管理测试用例
2、读取Excel中的数据
3、读取的数据进行接口传参
4、接口响应参数的断言
5、回写测试结果
6、生成测试报告
流程大家应该很清楚了吧,说白了,自动化嘛,无非就是将人工要做的事情用机器去实现,那么下面我们就按照流程为大家进行一 一剖析,在这之前,简单说下自动化框架的分层设计,大家可以下载一个pycharm社区版
该框架主要分为了case、common、config以及report四层,如下所示
其中:
case层:该层主要用来存放测试用例,用例格式主要就是接口中需要传入的参数(包括请求方法、url、body、headers、type等字段)、测试结果需要回写的参数、检查点等数据,如下所示
common层:该层级里面主要存放整个框架中需要用到的方法,如Excel表格数据读取方法、配置文件读取方法、设置执行结果颜色的方法等
config层:该层存放一些配置文件,如测试用例存放的地址,增强了代码的可移植性
report层:该层主要存放测试结果文件
好了,自动化分层我们已经知道是怎么回事了,那么接下来就进入我们的主角---自动化流程以及实现的方式,首先,我们看下case层中测试用例里面包含了那些东东O(∩_∩)O
看了上面这张图,应该知道了吧,其实用例里面主要存放的就是接口请求的参数,因为后面会有读表的方法,将这些参数读出来传到接口中进行请求,其中图中的"用例编号"就是作为一个标记,后面执行接口时候,会在报告中体现出来,可以直观的看到执行了那些用例;"检查点"的目的是为了后续做断言,以此来判断接口的执行结果是通过还是失败;"回写参数"的目的是为了用例执行完后,将接口返回结果中的一些参数回写回来,比如状态码(statuscode)、错误信息(error)、token等信息
接下来我们来看下common层中的各个方法是怎么实现的以及实现的目的(ps:所有的方法,里面都有详细的备注)
readexcel.py方法:用来读取测试用例中的数据(需要提前安装一个xlrd库(pip install xlrd)),读出来的数据存在一个列表中,直接上代码:
'''
读excel表的方法
'''
import xlrd
from common.readconfig import ReadConfig
class ReadExcel():
def __init__(self,excelPath,sheetName):
self.data = xlrd.open_workbook(excelPath)
self.table = self.data.sheet_by_name(sheetName)
#获取表中第一行值(表头)作为key值
self.ColName = self.table.row_values(0)
#总行数
self.rowNum = self.table.nrows
#总列数
self.colNum = self.table.ncols
def read_excel_data(self):
if self.rowNum<=1:
print("表的总行数小于1,没有请求参数值!")
else:
ExcelDataList=[]#接收读取Excel数据返回的值列表
j=1
for i in list(range(self.rowNum-1)):
ExcelDataDic={}#接收每行Excel的返回值
ExcelDataDic['rowNum']=i+2
values=self.table.row_values(j)
for x in list(range(self.colNum)):
ExcelDataDic[self.ColName[x]] = values[x]
ExcelDataList.append(ExcelDataDic)
j+=1
return ExcelDataList
if __name__=="__main__":
Rc=ReadConfig()
excelPath=Rc.get_excelPath()#从配置文件读取excelPath
sheetName=Rc.get_SheetName()#从配置文件读取sheetName
data=ReadExcel(excelPath,sheetName)
print(data.read_excel_data())
setcolor方法:将测试结果中pass和fail的用例用不同的颜色进行标记,代码如下
import pandas as pd
import openpyxl
from openpyxl.styles import Font
from common.readconfig import ReadConfig
class finddata():
def finddata_pass(self,filenames):
font_pass = Font(size=16, bold=True, color="00FF00")#设置pass的颜色为绿色
data=openpyxl.load_workbook(filenames)
table=data.active
pw = pd.read_excel(filenames) ##文件路径
index_pass = []#定义一个空列表,用于接收满足条件的表格单元行和列的值
for indexs in pw.index:
r = {}
for i in range(len(pw.loc[indexs].values)):
if (pw.loc[indexs].values[i] == 'pass'):
row_index = indexs + 1
col_index = i + 1
r['row_index'] = row_index
r['col_index'] = col_index
index_pass.append(r)
lenpass=len(index_pass)
for i in range(lenpass):
table.cell(row=index_pass[i]['row_index']+1, column=index_pass[i]['col_index']).font = font_pass
data.save(filenames)
return index_pass
def finddata_fail(self,filenames):
font_fail = Font(size=16, bold=True, color="FF0000") # 设置fail的颜色为红色
data = openpyxl.load_workbook(filenames)
table = data.active
pw = pd.read_excel(filenames) ##文件路径
index_fail = []
for indexs in pw.index:
r = {}
for i in range(len(pw.loc[indexs].values)):
if (pw.loc[indexs].values[i] == 'fail'):
row_index = indexs + 1
col_index = i + 1
r['row_index'] = row_index
r['col_index'] = col_index
index_fail.append(r)
lenpass = len(index_fail)
for i in range(lenpass):
table.cell(row=index_fail[i]['row_index'] + 1, column=index_fail[i]['col_index']).font = font_fail
data.save(filenames)
return index_fail
if __name__=="__main__":
Rc = ReadConfig()
resultfile=Rc.get_resultfile()
Fd=finddata()
print("pass:",Fd.finddata_pass(resultfile))
print("pass:", Fd.finddata_fail(resultfile))
writeexcel.py方法:进行excel表的复制,以及将数据写进excel表
import openpyxl
from common.readconfig import ReadConfig
def copy_excel(excelpath1,excelpath2):#excel表格的复制方法
wb2=openpyxl.Workbook()
wb2.save(excelpath2)
wb1=openpyxl.load_workbook(excelpath1)
wb2=openpyxl.load_workbook(excelpath2)
sheets1=wb1.sheetnames#获取被复制excel的sheets页
sheets2=wb2.sheetnames#获取复制目标excel的sheets页
sheet1=wb1[sheets1[0]]
sheet2=wb2[sheets2[0]]
max_row=sheet1.max_row#获取当前sheets页的最大行
max_column=sheet1.max_column#获取当前sheets页的最大列
for m in list(range(1,max_row+1)):
for n in list(range(97,97+max_column)):#chr(97)='a'
n=chr(n)#转换成ASCII码
i='%s%d'%(n,m)#单元格编号
celll=sheet1[i].value#获取单元格的值
sheet2[i]=celll#将对应的值复制到另一张表中
wb2.save(excelpath2)
wb1.close()
wb2.close()
class Write_excel(object):#写表的方法
def __init__(self,filename):
self.filename=filename
self.wb=openpyxl.load_workbook(self.filename)
self.ws=self.wb.active#取得活动页
def Write(self,row_n,col_n,value):
self.ws.cell(row_n,col_n).value=value
self.wb.save(self.filename)
self.wb.close()
if __name__ == "__main__":
Rc = ReadConfig()
excelpath1 = Rc.get_excelpath1() # 从配置文件读取excelpath1
excelpath2 = Rc.get_excelpath2() # 从配置文件读取excelpath2
copy_excel(excelpath1,excelpath2)
wt=Write_excel(excelpath1)
wt.Write(5,12,"Write")
readconfig.py方法:读取配置文件,如测试用例绝对路径、测试结果绝对路径等,统一将这些路径通过方法读取,以提高代码的可移植性
import configparser
cf = configparser.ConfigParser()
cf.read("E:\AutoScripts\config\config.ini")
class ReadConfig:
def get_excelPath(self):
excelPath = cf.get("dir", "excelPath")
return excelPath
def get_SheetName(self):
sheetName=cf.get("dir", "sheetName")
return sheetName
def get_excelpath1(self):
excelpath1=cf.get("dir", "excelpath1")
return excelpath1
def get_excelpath2(self):
excelpath2=cf.get("dir", "excelpath2")
return excelpath2
def get_resultfile(self):
resultfile=cf.get("dir", "resultfile")
return resultfile
def get_testfile(self):
testfile=cf.get("dir", "testfile")
return testfile
def get_resultfiles(self):
resultfiles=cf.get("dir", "resultfiles")
return resultfiles
if __name__=="__main__":
RC=ReadConfig()
print(RC.get_excelPath())
print(RC.get_SheetName())
print(RC.get_excelpath1())
print(RC.get_excelpath2())
print(RC.get_resultfile())
print(RC.get_testfile())
print(RC.get_resultfiles())
operate_connect_data.py方法:用来操作连接数据库的,主要用作数据初始化
#coding:utf-8
import pymysql as mdb
class Script_Connect_Database:
def script_connect_database(self,SqlEXP):#SqlEXP参数为需要执行的SQL语句表达式
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
filedir = "E:\\AutoScripts\\config\\dbconfig.xml"#数据库配置文件地址
root = ET.parse(filedir).getroot()
conn = mdb.connect(
host=root.find("Server").text,
user=root.find("Uid").text,
password=root.find("Pwd").text,
database=root.find("DataBase").text,
port=3306,
charset="utf8")
cur = conn.cursor()
SqlEXP = SqlEXP
cur.execute(SqlEXP)
conn.commit()#此处增加commit()方法
Result = cur.fetchall()
return Result
if __name__ == '__main__':
SCD=Script_Connect_Database()
ss=SCD.script_connect_database("SELECT * FROM `user` where id='0003fe13ddb311e992e600163e0ebb52'")
print(ss)
base_api.py方法:主要用来执行接口请求以及回写测试结果
import json
import requests
from common.readexcel import ReadExcel
from common.writeexcel import copy_excel,Write_excel
from common.readconfig import ReadConfig
#封装发送请求
def send_requests(s,testdata):
method=testdata["method"]
url=testdata["url"]
try:
params=eval(testdata["params"])
except:
params=None
try:
headers=eval(testdata["headers"])
except:
headers=None
type=testdata["type"]
test_nub=testdata['id']
print("*******正在执行用例:----- %s ----**********" % test_nub)
try:
bodydata=eval(testdata["body"])
except:
bodydata={}
if type=="data":
body=bodydata
elif type=="json":
body=json.dumps(bodydata)
else:
body = bodydata
#对请求类型是POST做判断
if method=="POST":
verify = False
res = {} # 接收请求结果返回参数
try:
rPost = s.request(method=method, url=url, params=params, headers=headers, data=body, verify=verify)
res['id'] = testdata['id']
res['rowNum'] = testdata['rowNum']
res["statuscode"] = str(rPost.status_code)
res["text"] = rPost.content.decode("utf-8")
if res["statuscode"] != "200":
res["error"] = res["text"]
else:
res["error"] = ""
res["msg"] = ""
if testdata["checkpoint"] in res["text"]:
res["result"] = "pass"
print("用例测试结果: %s---->%s" % (test_nub, res["result"]))
print("\t\t\t")
else:
res["result"] = "fail"
print("用例测试结果: %s---->%s" % (test_nub, res["result"]))
print("\t\t\t")
return res
except Exception as msg:
res["msg"] = str[msg]
return res
elif method == "GET":#对请求类型是GET做判断
res = {}
try:
Rc=ReadConfig()
resultfile=Rc.get_resultfile()
sheetName=Rc.get_SheetName()
token = ReadExcel(resultfile, sheetName).read_excel_data()[0]['token'] # 获取结果表中的token值
headers = {"Content-Type": "application/json;charset=utf-8", "X-Dts-Admin-Token": token}
rGet = requests.request('GET', url, headers=headers)
res['id'] = testdata['id']
res['rowNum'] = testdata['rowNum']
res["statuscode"] = str(rGet.status_code)
res["text"] = rGet.text
if res["statuscode"] != "200":
res["error"] = res["text"]
else:
res["error"] = ""
res["msg"] = ""
if testdata["checkpoint"] in res["text"]:
res["result"] = "pass"
print("用例测试结果: %s---->%s" % (test_nub, res["result"]))
print("\t\t\t")
else:
res["result"] = "fail"
print("用例测试结果: %s---->%s" % (test_nub, res["result"]))
print("\t\t\t")
return res
except Exception as msg:
res["msg"] = str[msg]
return res
#定义测试结果写入方法
def write_result(result,filename):
row_nub=result['rowNum']
wt= Write_excel(filename)
wt.Write(row_nub, 9, result['statuscode'])
wt.Write(row_nub, 10, result['error'])
wt.Write(row_nub, 11, result['result'])
wt.Write(row_nub, 12, result['msg'])
if result['id']=='CY001':
wt.Write(row_nub, 13, json.loads(result["text"])['data'])#只有测试用例ID为"CY001",才将token值写入结果表中(“CY001默认为登录用例”)
if __name__ == "__main__":
Rc = ReadConfig()
resultfile = Rc.get_resultfile()
excelPath = Rc.get_excelPath() # 从配置文件读取excelPath
sheetName = Rc.get_SheetName() # 从配置文件读取sheetName
copy_excel(excelPath,resultfile)#复制测试接口文件到结果文件
data = ReadExcel(excelPath,sheetName).read_excel_data()#获取测试接口文件数据
rowNumSum=ReadExcel(excelPath,sheetName).rowNum#获取测试接口文件总行数
s = requests.session()
for i in range(0,rowNumSum-1):#循环执行每个接口并回写测试结果数据
res = send_requests(s, data[i])
print("res:",res)
write_result(res, resultfile)
OK,所有方法准备就绪,最后我们需要的就是如何来运行测试用例,我把这个方法(test_api.py)放在case层的(这个看自己的使用习惯),下面具体看下这个方法是怎么实现的,代码如下:
import unittest
import ddt
import os
from common.base_api import *
from common.writeexcel import *
from common.setcolor import *
from common.readconfig import ReadConfig
Rc=ReadConfig()
testfile=Rc.get_testfile()
resultfiles=Rc.get_resultfiles()
sheetName=Rc.get_SheetName()
curpath = os.path.dirname(os.path.realpath(__file__))
testxlsx = os.path.join(curpath, testfile)#测试数据的地址
report_path = os.path.join(os.path.dirname(curpath), "report")
reportxlsx = os.path.join(report_path, resultfiles)#测试结果地址
testdata=ReadExcel(testxlsx,sheetName).read_excel_data()#测试数据读取
@ddt.ddt
class Test_api(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.s=requests.session()
copy_excel(testxlsx,reportxlsx)#复制测试数据到report目录
@ddt.data(*testdata)
def test_api(self,data):
res=send_requests(self.s,data)
write_result(res,reportxlsx)
check=data["checkpoint"]
res_text=res["text"]
self.assertTrue(check in res_text)
@classmethod
def tearDownClass(self):
fd=finddata()
fd.finddata_pass(reportxlsx)
fd.finddata_fail(reportxlsx)
if __name__ == "__main__":
unittest.main()
或者通过用例集的方式,顺带生成测试报告
import unittest
import time
from HTMLTestRunner import HTMLTestRunner
from common.setcolor import finddata
case_dir='E:\AutoScripts\case'
discover=unittest.defaultTestLoader.discover(case_dir,pattern="test*.py",top_level_dir=None)
if __name__ == '__main__':
now=time.strftime("%Y-%m-%d %H-%M-%S")
fp=open('report/'+now+'report.HTML','wb')
runner=HTMLTestRunner(stream=fp,title="测试报告",description="XXX系统测试报告")
runner.run(discover)
fp.close()
#将测试结果文件中的pass和fail的用例结果设置颜色
filenames = "E:\AutoScripts\\report\\result.xlsx"
Fd = finddata()
Fd.finddata_pass(filenames)
Fd.finddata_fail(filenames)
运行完后,我们可以看下最终的测试结果以及测试报告样式