项目自动化框架设计

空余时间写了一个接口自动化测试框架,为后期项目自动化测试规划奠定了一些基础的东西,顺便记录一下整个接口自动化框架设计过程中的一些心得和体会,写这篇文章的目的旨在分享和交流,自动化小白读完这篇文章,应该能够对接口自动化有一个很好的轮廓和理解

OK,首先说下这次框架设计的基础原理,框架主要采用的是python+excel+request+unittest方式,主要包含以下流程

1、excel管理测试用例

2、读取Excel中的数据

3、读取的数据进行接口传参

4、接口响应参数的断言

5、回写测试结果

6、生成测试报告


流程大家应该很清楚了吧,说白了,自动化嘛,无非就是将人工要做的事情用机器去实现,那么下面我们就按照流程为大家进行一 一剖析,在这之前,简单说下自动化框架的分层设计,大家可以下载一个pycharm社区版


该框架主要分为了case、common、config以及report四层,如下所示


image.png

其中:

case层:该层主要用来存放测试用例,用例格式主要就是接口中需要传入的参数(包括请求方法、url、body、headers、type等字段)、测试结果需要回写的参数、检查点等数据,如下所示

image

common层:该层级里面主要存放整个框架中需要用到的方法,如Excel表格数据读取方法、配置文件读取方法、设置执行结果颜色的方法等

image

config层:该层存放一些配置文件,如测试用例存放的地址,增强了代码的可移植性

image

report层:该层主要存放测试结果文件

image

好了,自动化分层我们已经知道是怎么回事了,那么接下来就进入我们的主角---自动化流程以及实现的方式,首先,我们看下case层中测试用例里面包含了那些东东O(∩_∩)O

image

看了上面这张图,应该知道了吧,其实用例里面主要存放的就是接口请求的参数,因为后面会有读表的方法,将这些参数读出来传到接口中进行请求,其中图中的"用例编号"就是作为一个标记,后面执行接口时候,会在报告中体现出来,可以直观的看到执行了那些用例;"检查点"的目的是为了后续做断言,以此来判断接口的执行结果是通过还是失败;"回写参数"的目的是为了用例执行完后,将接口返回结果中的一些参数回写回来,比如状态码(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)

运行完后,我们可以看下最终的测试结果以及测试报告样式


image.png
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342