Python從無到有搭建接口(API)自動化測試框架


轉自:https://www.csdn.net/tags/MtTaMgwsMTg2MjI4LWJsb2cO0O0O.html

Python從無到有搭建接口(API)自動化測試框架    2021-01-03 23:51:03

  • 目錄

    1、前言

    2、思路    

    3、正文

    一、路徑模塊-initPath.py

    二、配置文件模塊-getConfig.py

    三、讀取用例模塊-getCase.py

    四、數據庫操作模塊-operatorDB.py

    五、日志模塊-log.py

    六、郵件模塊-sendEmail.py

    七、消息模塊-sendMsg.py

    八、變量參數化模塊-parameteriZation.py

    九、API請求模塊-sendApirequests.py

    十、公共方法的模塊(核心)-commKeyword.py

    十一、工廠封裝(核心)-methodFactoy.py

    十二、解析用例(核心)-testCase.py

    十三、最后的運行文件-testRun.py


     

    1、前言

             自動化測試,是測試道路上不可或缺的重要部分,現在有很多自動測試工具,基本可以滿足軟件市場的測試要求,但使用工具讓人知其然而不知其所以然,學會了也只是一個自動化測試工具人,所以學會自動化框架,是擺脫工具人、提升自己、加薪升職的必經之路;

             天王劉德華說:學到了要教人。

             這是一個已經學會了的人分享的一點知識,希望測試同胞們在前進路上路過我這篇博客時,如得感覺有一點點的幫助,請留下您的一個贊。

             觀看此文時,需要有入門級的Python代碼基礎,如沒有,請先去找視頻教程學習一段時間。

             本文所有講解代碼與執行結果用截圖展示,這為了是讓您看着可以有個寫代碼的過程,提升自己;當然如果不想寫,也可以只接跳轉每一小節末尾,整體代碼展示,可以"使用程序員高階技能ctrl+c, ctrl+v"自行學習。

     

            代碼庫:魂尾/testApi

    2、思路    

           1、搭建一個目錄框架

                 如下圖

         

                common目錄里內容含義

                 setApirequest.py  實現API接口請求的模塊,實現發送post/get等方法;      

                 getCase.py  實現讀取data目錄里的用例文件數據的,設計執行格式,輸出JSON用例集;

                 getConfig.py 實現讀取config目錄下的配置文件;

                 initPath.py  實現獲取框架文件路徑,方便其他模塊使用目錄方法;

                 log.py  實現日志打印統一入口,將文件輸出到log目錄里;

                 operatorDB.py  實現讀寫數據的方法;

                 parameteriZation.py  實現數據參數化的實體類;

                 sendEmail.py\ SendMsg.py  實現實時發送測試報告至郵件與企業微信的方法;

                kemel目錄里內容含義

                 methodFactory.py 實現各方法調用的統一入口;

                 commKeyworl.py 公共方法、主要是分裝所有底層模塊的操作入口,並提供一些特殊性的公共方法;

                testcase目錄里內容含義

                 圖片里目錄中沒有添加文件。應該是這個文件tsetCase.py,實現解析getCase.py里輸出的JSON用例集,執行用例的,檢查執行結果的模塊,因為Unitest庫的要求,此用例解析目錄與文件必須以test開頭,當然含義也見名思義。

               其他目錄 

                data用例文件目錄,log輸出日志目錄、report輸出測試報告目錄,library引入三方模塊目錄(ddt數據驅動模塊,HTMLRunner測試報告格式化輸出模塊)

                 library引入兩個三方庫的下載路徑(重要文件,不或或缺):

                 ddt:自動代測試數據驅動ddt.py-互聯網文檔類資源-CSDN下載

                 HTMLRunner:   HTMLTestRunnerNew.py針對《Python從無到有搭建接口(API)自動化測試框架》的測試報告,非此框架勿下載_htmltestrunnernew-互聯網文檔類資源-CSDN下載

       

           5、分層概念

               一個軟件MCV的層次概念

                M是底層,模型層,封裝功能的代碼,屬於底層代碼 。

                C是中層,控制層,封裝所有底層的接入方法。

                V是高層,會話層,運行執行等方法操作。

               MVC的區分(分層概念)

               common目錄下的模塊都是M

               kemel目錄下的模塊是C

               test_Case.py, testRun.py等運行模塊是V

                

    3、正文

    一、路徑模塊-initPath.py

           1、了解如何獲取當前絕對路徑:

                 先了解python是如何取得當前目錄的,我們可以使用內置庫os來實現

                 首先我們得知道 __file__在python中代表當前文件,

                 然后在菜鳥教程里找到os.path的庫,知道獲取絕對路徑的方法 os.path.abspath(path)

                 然后開始在initPath.py寫代碼打印一下os.path.abspath(__file__)方法

                 見下圖,打印出了initPath.py的絕對路徑。

            2、獲取文件路徑,不包含本身文件名

                  在菜鳥找到了os.path,dirname()方法

                  在initPath.py里使用打印一下這個方法,然后我們希望是\的路徑,所以使用os.path.dirname(os.path.abspath(__file__))這個代碼取路徑

                  然后我們再增加一層,os.path.dirname(os.path.dirname(os.path.abspath(__file__))),我們是要獲取工程根目錄路徑的。如下圖

            3、拼接所有路徑

                  我們要拼接目錄,需要一個方法,進入菜鳥網,找到os.path.join(path1, path2),合成路徑方法

                 直接寫initPath.py的代碼了,將項目下所有文件夾的路徑都定義好了。打印如下圖

     

                 initPath.py的代碼段

    import os
    #get project dir
    BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    #get common dir
    COMMONDIR = os.path.join(BASEDIR, 'common')
    #get config dir
    CONFDIR = os.path.join(BASEDIR, 'config')
    #get data dir
    DATADIR = os.path.join(BASEDIR, 'data')
    #get library dir
    LIBDIR = os.path.join(BASEDIR, 'library')
    #get log dir
    LOGDIR = os.path.join(BASEDIR, 'log')
    #get report dir
    REPORTDIR = os.path.join(BASEDIR, 'report')
    #get testcaset dir
    CASEDIR = os.path.join(BASEDIR, 'testcases')

     

     

    二、配置文件模塊-getConfig.py

            1、我們先了解一下配置文件

                  在計算機領域,配置文件:是一種計算機文件,可以為一些計算機程序配置參數和初始設置。

                  具體文件類型與相關知識點不多說,我們這里使用一ini配置文件,內容格式如下

                 [about]

                 aaa = bbb

                 ccc = ddd

                 其實about是節點,aaa = bbb是參數,key = value的意思

                 所以在config/baseCon.ini下配置添加一些配置,如下圖

             2、讀取baseCon.ini里的配置項

                  獲取配置文件路徑:

                  讀取配置文件之前,我們得先得到文件baseCon.ini的絕對路徑,

                  先引用已經定義好的config的絕對路徑,然后使用os.path.join(path1, path2)方法將baseCon.ini的絕對路徑生成,具體代碼如下圖

                 了解configparser

                  ConfigParser 是用來讀取配置文件的包。三方庫,所以需要通過命令 pip install configparser 來下載。

                  在代碼里直接導入ConfigParser類,然后創建其對像,調用方法read(),讀取配置文件,具體代碼如下圖

                   仔細學習上面的路子,下面就開始着手封裝configparser了,這個庫已經很完美了,不過我們后續讀取配置文件時需要將讀取文件這個步驟也省略,所以稍加封裝。

                   封裝configparser配置文件讀取

                   封裝的代碼運行如下圖

                 

                 nyCof.saveData()方法添加了配置項,寫到了baseCon.ini里面

                getConfig.py的代碼段:

    
    import os
    from common.initPath import CONFDIR
    from configparser import ConfigParser
    # conPath = os.path.join(CONFDIR, 'baseCon.ini')
    # print(conPath)
    # cnf = ConfigParser()
    # cnf.read(conPath, encoding='utf-8')   #第一個參數是文件路徑,第二個參數是讀取的編碼
    #
    # print('baseCon.ini里所有節點{}'.format(cnf.sections())) #打印所有節點名稱
    # print('db下的所有key:{}'.format(cnf.options('db')))  #打印db節點下的所有key
    # print('db下的所有Item:{}'.format(cnf.items('db')))    #打印db節點下的所有item
    # print('db下的host的value:{}'.format(cnf.get('db', 'host')))  #打印某個節點的某個value
    
    """
    定義Config繼續ConfigParser
    """
    class Config(ConfigParser):
    
        def __init__(self):
            """
            初始化
            將配置文件讀取出來
            super().    調用父類
            """
            self.conf_name = os.path.join(CONFDIR, 'baseCon.ini')
            super().__init__()
            super().read(self.conf_name, encoding='utf-8')
    
        def getAllsections(self):
            """
            :return: 返回所有的節點名稱
            """
            return super().sections()
    
        def getOptions(self, sectioName):
            """
            :param sectioName: 節點名稱
            :return: 返回節點所有的key
            """
            return super().options(sectioName)
    
        def getItems(self, sectioName):
            """
            :param sectioName: 節點名稱
            :return: 返回節點的所有item
            """
            return super().items(sectioName)
    
        def getValue(self, sectioName, key):
            """
            :param sectioName: 節點的名稱
            :param key: key名稱
            :return: 返回sectioName下key 的value
            """
            return super().get(sectioName, key)
    
        def saveData(self, sectioName, key, value):
            """
            添加配置
            :param sectioName: 節點名稱
            :param key: key名
            :param value: 值
            :return:
            """
            super().set(section=sectioName, option=key, value=value)
            super().write(fp=open(self.conf_name, 'w'))
    
    
    myCof = Config()
    #print(myCof.getAllsections())
    #print(myCof.getOptions('db'))
    #print(myCof.getItems('db'))
    #print(myCof.getValue('db', 'host'))
    #myCof.saveData('db', 'newKey', 'newValue')

     

     

    三、讀取用例模塊-getCase.py

              讀取用例是框架的比較重要,與獨立的模塊,不過讀取用例前,我們先要設計用例文件與用戶格式,一般我們可以將用例放在excel里,或者建立一個mysql數據庫,將excel里的數據導入到里面,但后者比較麻煩,所以本文只接讀取excel文件里的用例

             1、設計用例文件與用例格式

                  在data目錄里新增一個testcase.xlsx的excel文件,目錄如下圖:

                  打開testcase.xlsx,在sheet1里設計用例表頭,如下圖:

                 用例字段的含義:

                  case_id:id,自己設定數字,只為了輸出報告時識別用例的位置

                  api:url里的接口路由,也是為了輸出報告時識別用例的正確性

                  title: 可以是用例的中文標題,也可以自定義方法中參數化接收數據的變量,格式可以是,abc或${abc}

                  method: 請求類型 post/get ,或者自定義的方法名稱

                  url: 接口請求中的url,可以被參數化

                  headers: 接口請求中的headers, 可以被參數化

                  data:  接口請求中的data, 可以被參數化

                  checkey:用例檢查點的檢查方法

                  expected:  用例檢查點的檢查值

                  Test_result:  用例執行完成后,回寫給用例的狀態,pass/fail,不過我一般不用。

                 用例示例如下圖:

              

             2、設計用例集JSON格式 

                   格式如下:

                 [   #用例集
    
                      {    #第一條用例
    
                         key1: value1,    #用例里的字段與字段值
    
                         key2: value2
    
                         ...
    
                       },
    
                       {  #第二條用例
    
                           key1:value1,  #用例里的字段與字段
    
                           key2: value2
    
                           ...
    
                        },
    
                        .... 
    
                  ]

                        根據用例excel文件,具體的JSON用例串應該下面是這樣子的,前面字段是excel列表數據,最后一個是sheet_name是表示了sheet頁名稱。一般接口用例按模塊編寫,一個模塊一個sheet頁,以此字段區分,為后面輸出測試報告做准備。

    [

          {

                'case_id':1,

                'api': 'publickey',

                 'title':'url',

                 'method':'設置變量',

                 'url':'https://www.adbcde.com.cn/',

                 'headers':'Host:www.adbcde.com.cn',

                 'data':'userToken=16029513602050&loginpassword=qwert12345&loginphone=15361828291',

                 'checkey':'結果包含',

                 'expected':'"return_code": "SUCCESS"',

                 'Test result':None,

                 'sheet_name':'sheet1',

           },

           {

              ......

           }

    .....

    ]

     

             3、openpyxl庫學習

                   讀寫excel的庫有很多,因為用例要求不高,所以選擇openpyxl庫來封裝用例代碼

                   安裝使用命令 pip install openpyxl

                   開始寫代碼

                   操作excel用例之前,要得到文件的絕對路徑

                   獲取絕對路徑成功,接下來開始讀取excel里的文件了,

                   讀取excel文件內容

                    步聚如下圖,1、先打開excel文件,2、遍歷一下sheet頁輸出名稱,3遍歷輸出Sheet1里的數據,這三個寫后,就表名,excel可以讀取成功了。openpyxl也學完了

                 4、封裝用例類

                      我定義的用例是一個excel文件一個項目,在工作中,應該不止一個項目,所以有可能在data目錄里有多個excel文件,我們需要執行哪個呢,所以此能用到前面寫的配置文件,在里面加一個case節點,增加執行用例文件名稱

                      一個用例文件中肯定有很多注釋的用例,所以定義一個 # 來區分注釋用例。兩個定義如下圖

                          封裝代碼無法載圖全部,先看執行結果,后面讀取用例只需要兩行代碼就可將用例JSON讀取出來

                         getCase.py的代碼段

    import os
    import openpyxl
    from common.initPath import DATADIR
    from common.getConfig import myCof
    
    # #拼接用例文件絕對路徑
    # caseFile = os.path.join(DATADIR, 'testcase.xlsx')
    # print(caseFile)
    # #讀取excel文件
    # xl = openpyxl.open(filename=caseFile)
    # #打印caseFile里的sheet頁名稱
    # print('打印所有sheet頁')
    # for sheet in  xl:
    #     print(sheet.title)
    # #打印excel里的所有的行字段數據
    # print('打印Sheet1里的所有的行字段數據')
    # sh = xl['Sheet1']
    # data = list(sh.rows)
    # for da in data:
    #     for k in da:
    #         print(k.value)
    
    
    class Getcase(object):
        def __init__(self, sheet_name=None):
            """
            初始化文件名稱,sheet名稱,excel對像
            :param sheet_name: 傳入的sheet名稱 ,可以為空。
            """
            filename = myCof.getValue('case', 'testCase')
            self.note = myCof.getValue('identifier', 'note')
            self.caseFile = os.path.join(DATADIR, filename)
            self.sheet_name = sheet_name
            self.wb = None
    
        def openexcel(self):
            """
            打開excel文件
            如果sheet名稱不為空,定位到對應sheet頁
            :return:
            """
            self.wb = openpyxl.open(self.caseFile)
            if self.sheet_name is not None:
                self.sh = self.wb[self.sheet_name]
    
    
        def read_excels(self):
            """
            格式化用例集
            用例格式JSON見上面的前面的描述
            過濾掉#注釋的用例
            :return:
            """
            if self.wb is None:
                self.openexcel()
            datas = list(self.sh.rows)
            title = [i.value for i in datas[0]]
            cases = []
            for i in datas[1:]:
                data = [k.value for k in i]
                case = dict(zip(title, data))   #將數據格式化中JSON串
                try:
                    if str(case['case_id'])[0] is not self.note:  # 過濾掉note符號開頭的用例,注釋掉不收集、不執行
                        case['sheet'] = self.sh.title
                        cases.append(case)
                except KeyError:
                    cases.append(case)
            return cases
    
        def read_all_excels(self):
            """
            遍歷所有的sheet頁
            取得所有用例集,再格式下一次,
            過濾掉#注釋的sheet頁
            :return:
            """
            self.openexcel()
            cases = []
            for sheet in self.wb:
                if sheet.title[0] is not self.note:  # 過濾掉note符號開頭的sheet頁,注釋掉的不收集,不執行
                    self.sh = sheet
                    cases += self.read_excels()
            return cases
    
    
        def write_excels(self, rows, column, value):
            """
            回寫用例字段
            :param rows:
            :param column:
            :param value:
            :return:
            """
            self.openexcel()
            self.sh.cell(row=rows, column=column, value=value)
            self.wb.save(self.caseFile)
    
    
    # readExce = Getcase()
    # print(readExce.read_all_excels())
    

     

    四、數據庫操作模塊-operatorDB.py

             1、pymysql安裝

                   安裝pymysql使用命令pip install pymysql

     

             2、pymysql學習

                   學習之前,我們先把連接數據庫的相關配置參數,在配置文件中取出來

                   如下圖,先導入myCof對像,使用getValue取出對應該的配置參數打印成功

                   pymysql已經導入成功了,連接數據參數也取出來了,接下來開始連接數據,執行SQL語句

                   連接數據庫

                   如果沒連接成功會拋出異常,所以此時需要 try ... except.....來catch異常,打印出來,下圖為連接數據庫超時,因為還沒起Mysql的服務。

                    這里我們得先配置好一個mysql服務器,方便調試,不懂的可以在網上找教程學習學習,當然直接用公司的測試環境也行,省時少力。

                   打開mysql服務后,運行,會報一個錯,我們將port參數強轉化為int類型,他不能為str類型。

                   強轉后,再運行一次,連接成功了

                   執行SQL

                   執行SQL語句,我們需要游標,先獲取游標,再使用游標執行SQL,一次通過

                   打印SQL查詢的數據

                   pymysql學習完成,接下來可以封裝了

             3、封裝數據操作

                   封裝的邏輯是,將連接作一個方法,執行SQL寫一個方法,關閉連接寫一個方法

                   先看封裝好后的執行結果,只需要寫四行代碼就可以執行SQL語句了。

                    opeartorDB.py的代碼段:

    import pymysql
    from common.getConfig import myCof
    
    # host = myCof.getValue('db', 'host')
    # port = int(myCof.getValue('db', 'port'))
    # user = myCof.getValue('db', 'user')
    # pwd = myCof.getValue('db', 'pwd')
    # database = myCof.getValue('db', 'database')
    # charset = myCof.getValue('db', 'charset')
    # try:
    #     #連接數據庫
    #     db = pymysql.connect(host=host, port=port, user=user, password=pwd, database=database, charset=charset)
    #     #獲取游標
    #     cursor = db.cursor()  
    #     #執行SQL
    #     cursor.execute("select * from Student where SName = '林小六';")
    #     #獲取查詢結果
    #     result = cursor.fetchall()
    #     #打印查詢結果
    #     print(result)
    #     print('執行成功')
    # except Exception as e:
    #     print('連接失敗,原因:{}'.format(str(e)))
    """
    封裝mysql操作
    """
    class OpeartorDB(object):
        def __init__(self):
            """
            初始化方法,習慣性留着
            """
            pass
    
        def connectDB(self):
            """
            連接數據庫
            :return: 返回成功失敗,原因
            """
            host = myCof.getValue('db', 'host')
            port = myCof.getValue('db', 'port')
            user = myCof.getValue('db', 'user')
            pwd = myCof.getValue('db', 'pwd')
            database = myCof.getValue('db', 'database')
            charset = myCof.getValue('db', 'charset')
            try:
                self.db = pymysql.connect(host=host, port=int(port), user=user, password=pwd, database=database, charset=charset)
                return True, '連接數據成功'
            except Exception as e:
                return False, '連接數據失敗【' + str(e) + '】'
    
    
        def closeDB(self):
            """
            關閉數據連接,不關閉會導致數據連接數不能釋放,影響數據庫性能
            :return:
            """
            self.db.close()
    
        def excetSql(self, enpsql):
            """
            執行sql方法,
            :param enpsql: 傳入的sql語句
            :return: 返回成功與執行結果 或 失敗與失敗原因
            """
            isOK, result = self.connectDB()
            if isOK is False:
                return isOK, result
            try:
                cursor = self.db.cursor()
                cursor.execute(enpsql)
                res = cursor.fetchone()     #為了自動化測試的速度,一般場景所以只取一條數據
                if res is not None and 'select' in enpsql.lower():    #判斷是不是查詢,
                    des = cursor.description[0]
                    result = dict(zip(des, res))   #將返回數據格式化成JSON串
                elif res is None and ('insert' in enpsql.lower() or 'update' in enpsql.lower()):               #判斷是不是插入或者更新數據
                    self.db.commit()       #提交數據操作,不然插入或者更新,數據只會更新在緩存,沒正式落庫
                    result = ''       #操作數據,不需要返回數據
                cursor.close()      #關閉游標
                self.closeDB()      #關閉數據連接
                return True, result
            except Exception as e:
                return False, 'SQL執行失敗,原因:[' + str(e) + ']'
    
    
    
    
    
    # sql = 'select * from Student'
    # oper = OpeartorDB()
    # isOK, result = oper.excetSql(sql)
    # print(result)
    
    
    

     

    五、日志模塊-log.py

     

             1、logging學習

                   logging是python的基礎庫,不需要下載,直接導入可用

                   日志有五個等級,自動測試一般INFO等都打印,所以我們在配置文件里的加上日志參數配置

                    [log]

                    level = INFO

                   打印日志

                   編寫代碼,先獲取日志等級配置,然后設置日志等級,初始化日志對像,打印日志,因為日志等是INFO,所以debug的日志不會打印,代碼如下圖

                   設置日志格式

                    格式設置如下圖

     

     

                    將日志輸出到文件

                    日志文件存放在log目錄下,所以先獲取導入目錄與os

                    設計日志文件隔一天時間,日志就另新增一個,保留十五天,所以需要導入logging里的一個方法TimedRotatingFileHandler、

    from logging.handlers import TimedRotatingFileHandler  #導入的方法

                     代碼如下圖

                    運行后,輸出了testReport文件,里面打印了執行日志

                    logging基本學習完成 ,再簡單封裝一下

     

             2、日志模塊封裝

                   封裝日志這一塊,不需要創建對像,因為他本身需要返回一個logging的對象,對象操作對象,別扭,所以在Log類里直接封裝一個靜態的方法,可以直接類調用方法返回一個logging對象。

                   調式執行結果與上面一致,但不截圖了,直接上代碼

                   log.py代碼段

    import os
    import logging
    from common.getConfig import myCof
    from common.initPath import LOGDIR
    from logging.handlers import TimedRotatingFileHandler
    
    # # 獲取日志等配置參數
    # level = myCof.getValue('log', 'level')
    # # 設置日志格式,%(asctime)s表示時間,%(name)s表示傳入的標識名,%(levelname)s表示日志等級,%(message)s表示日志消息
    # format = '%(asctime)s - %(name)s-%(levelname)s: %(message)s'
    # # 設置日志基礎等級, 設置
    # logging.basicConfig(level=level, format=format)
    # # 初始化日志對像,Hunwei是name
    # mylog = logging.getLogger('Hunwei')
    # #拼接日志目錄
    # log_path = os.path.join(LOGDIR, 'testReport')
    # #生成文件句柄,filename是文件路徑,when表是時間D表示天,backuCount=15目錄下最多15個日志文件,enccoding='utf-8'日志字符格式
    # fh = TimedRotatingFileHandler(filename=log_path, when="D", backupCount=15, encoding='utf-8')
    # #設置歷史日志文件名稱的格式,會自動按照某天生成對應的日志
    # fh.suffix = "%Y-%m-%d.log"
    # #設置文件輸出的日志等級
    # fh.setLevel(level)
    # #設置文件輸出的日志格式
    # fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s-%(levelname)s: %(message)s"))
    # #將文件句柄加入日志對象
    # mylog.addHandler(fh)
    #
    # mylog.debug('debug')
    # mylog.info('info')
    # mylog.warn('warm')
    # mylog.error('error')
    # mylog.fatal('fatal')
    
    
    
    class Log(object):
    
        @staticmethod
        def getMylog():
            # 獲取日志等配置參數
            level = myCof.getValue('log', 'level')
            # 設置日志格式,%(asctime)s表示時間,%(name)s表示傳入的標識名,%(levelname)s表示日志等級,%(message)s表示日志消息
            format = '%(asctime)s - %(name)s-%(levelname)s: %(message)s'
            # 設置日志基礎等級, 設置
            logging.basicConfig(level=level, format=format)
            # 初始化日志對像,Hunwei是name
            mylog = logging.getLogger('Hunwei')
            # 拼接日志目錄
            log_path = os.path.join(LOGDIR, 'testReport')
            # 生成文件句柄,filename是文件路徑,when表是時間D表示天,backuCount=15目錄下最多15個日志文件,enccoding='utf-8'日志字符格式
            fh = TimedRotatingFileHandler(filename=log_path, when="D", backupCount=15, encoding='utf-8')
            # 設置歷史日志文件名稱的格式,會自動按照某天生成對應的日志
            fh.suffix = "%Y-%m-%d.log"
            # 設置文件輸出的日志等級
            fh.setLevel(level)
            # 設置文件輸出的日志格式
            fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s-%(levelname)s: %(message)s"))
            # 將文件句柄加入日志對象
            mylog.addHandler(fh)
            #返回logging對像
            return mylog
    
    #調式代碼
    mylog = Log.getMylog()
    # mylog.debug('debug')
    # mylog.info('info')
    # mylog.warn('warm')
    # mylog.error('error')
    # mylog.fatal('fatal')

     

    六、郵件模塊-sendEmail.py

              1、開啟郵箱的SMTP服務

                    email郵箱提供商都可以開啟smtp服務的,如果知道什么是smtp並知道設置的朋友可以略過這一段

                   以qq郵箱為例

                   進入qqmail.com登錄郵箱,找到【設置】-【賬戶】,點擊POP3/SMTP 開啟(下圖標記的有誤,別被誤導了哈)

                   按描述發送短信

                   開啟之后,我們會得到一個密鑰,好好保存,

     

     

              2、學習使用smtplib庫發送帶附件的郵件

                   郵件參數添加到配置文件

                   host 是郵件服務器,騰訊的是smtp.qq.com,  

                   port 是郵件服務器端口,開通smtp時,騰訊會郵件告之端口號

                   user是郵箱、pwd是開通smtp時得到的密鑰

                   from_addr 是發送郵箱地址,與user是同一個

                   to_addr是收件箱,可以做成逗號分隔

                       連接smtp服務器

                       一般連接啥服務器沒成功都會拋異常呀,所以用一下try,新建一個smtplib的對像,帶上服務器與端口,然后使用用戶名密碼連接  

                        發送郵件

                        發送郵件之前需要構建一個郵件內容,所以所以email庫,可以通過pip install email下載,使用

                        先構建一個純文本的內容 ,所以導入 MIMEText,

                        下面是構建郵件內容與發送成功的截圖,msg消息休是郵件內容 ,里面需要文本,發送人,收件人,郵件主題等參數

                        發送成功后,進入郵箱查看郵件

                   郵件發送成功后了,基礎的已經學會了,還有兩種郵件類型、MIMEMultipart多媒體內型,MIMEApplication附件內型,不多贅述,看后面封裝代碼即可通明

                   

              3、封裝代碼

                    封裝代碼前呢,先知道了一般的自動化報告是html格式的,這里我拿了以前的測試放在工程report目錄下,方便使用,如下圖

                    封裝郵件模塊后,兩行代碼即可發送測試報告。

               

                    sendEmail.py的代碼段

     

    import os
    import smtplib
    from common.getConfig import myCof
    from email.mime.text import MIMEText  #導入純文本格式
    from email.mime.multipart import MIMEMultipart
    from email.mime.application import MIMEApplication
    from common.initPath import REPORTDIR
    
    # host = myCof.getValue('email', 'host')
    # port = int(myCof.getValue('email', 'port'))
    # user = myCof.getValue('email', 'user')
    # pwd = myCof.getValue('email', 'pwd')
    # to_addr = myCof.getValue('email', 'to_addr')
    # #定義純文本消息 ,From定義發件人, To定義收件人, Subject定義郵件標題
    # msg = MIMEText('hello,send by python_test...','plain','utf-8')
    # msg['From'] = user
    # msg['To'] = to_addr
    # msg['Subject'] = '測試郵件發送'
    # try:
    #     #連接smtp服務對話,創建對像
    #     smtp = smtplib.SMTP_SSL(host=host, port=port)
    #     #登錄服務器
    #     smtp.login(user=user, password=pwd)
    #     # 發送郵件
    #     smtp.sendmail(from_addr=user, to_addrs=to_addr, msg=msg.as_string())
    #     # 結束與服務器的對話
    #     smtp.quit()
    #     print('發送郵件成功')
    # except Exception as e:
    #     print('發送郵件失敗,原因:{}'.format(str(e)))
    
    class SendMail(object):
    
        def __init__(self):
            """
            初始化文件路徑與相關配置
            """
            all_path = []
            #獲取測試報告目錄下的報告文件名稱
            for maindir, subdir, file_list in os.walk(REPORTDIR):
                pass
    
            #拼接文件絕對路徑
            for filename in file_list:
                all_path.append(os.path.join(REPORTDIR, filename))
            self.filename = all_path[0]
            self.host = myCof.get('email', 'host')
            self.port = myCof.get('email', 'port')
            self.user = myCof.get('email', 'user')
            self.pwd = myCof.get('email', 'pwd')
            self.from_addr = myCof.get('email', 'from_addr')
            self.to_addr = myCof.get('email', 'to_addr')
    
    
    
        def get_email_host_smtp(self):
            """
            連接stmp服務器
            :return:
            """
            try:
                self.smtp = smtplib.SMTP_SSL(host=self.host, port=self.port)
                self.smtp.login(user=self.user, password=self.pwd)
                return True, '連接成功'
            except Exception as e:
                return False, '連接郵箱服務器失敗,原因:' + str(e)
    
    
        def made_msg(self):
            """
            構建一封郵件
            :return:
            """
            # 新增一個多組件郵件
            self.msg = MIMEMultipart()
    
            with open(self.filename, 'rb') as f:
                content = f.read()
            # 創建文本內容
            text_msg = MIMEText(content, _subtype='html', _charset='utf8')
            # 添加到多組件的郵件中
            self.msg.attach(text_msg)
            # 創建郵件的附件
            report_file = MIMEApplication(content)
            report_file.add_header('Content-Disposition', 'attachment', filename=str.split(self.filename, '\\').pop())
    
            self.msg.attach(report_file)
            # 主題
            self.msg['subject'] = '自動化測試報告'
            # 發件人
            self.msg['From'] = self.from_addr
            # 收件人
            self.msg['To'] = self.to_addr
    
    
        def send_email(self):
            """
            發送郵件
            :return:
            """
            isOK, result = self.get_email_host_smtp()
            if isOK:
                self.made_msg()
                self.smtp.send_message(self.msg, from_addr=self.from_addr, to_addrs=self.to_addr)
            else:
                return isOK, result
    
    
    # abc = SendMail()
    # abc.send_email()
    
    

     

    七、消息模塊-sendMsg.py

              1、創建企業微信應用           

                   在企業微信建立一個應用,為接收消息的載體,添加相關人員

                   在Python中實現得到企業微信應用token,

                   在企業微信官網創建一個公司,或者用公司的企業微信號,獲取企業微信的企業ID

                  創建應用

     

                   得到AgentId、Secret

                   進入新建的應用詳情頁面,可以得到這兩個字段

           1、學習發送企業微信消息

                 資深開發者直接會找企業微信的API開發指南,應該知道怎么封裝了。

                  先將創建應該時得到的三個參數配置到baseCon.ini文件里

                     

                 獲取企業微信的token

                 拼接得獲取token的API url,corpid與corpsecret字段為參數,用requests.get()方法請求,從結果集中解析出token字段值

                    發送消息

                   發送消息的代碼如下圖,先拼接發送消息的api的url,需要添加上面得到token值,再構造一個消息,轉換成bytes格式的消息休,使用requests.post發送消息

    企業微信收到了消息,如下圖

           2、代碼封裝

                封裝完成后,只需要兩行代碼就可以發送消息了

               sendMsg.py的代碼段

    
    import requests
    import json
    from common.getConfig import myCof
    
    # # 獲取企業微信的參數
    # corpid = myCof.get('wechat', 'corpid')
    # corpsecret = myCof.get('wechat', 'corpsecret')
    # agentid = myCof.get('wechat', 'agentid')
    # # 拼接獲取token的API
    # url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=' + corpid + '&corpsecret=' + corpsecret
    # # 使用requests請求API,轉為JSON格式
    # response = requests.get(url)
    # res = response.json()
    # #獲取token打印
    # token = res['access_token']
    # print(token)
    # # 拼接發送消息的api
    # url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=' + token
    # # 構建一個消息JSON串
    # jsonmsg = {
    #                "touser" : "@all",
    #                "msgtype" : "text",
    #                "agentid" : agentid,
    #                "text" : {
    #                    "content" : "API接口從無到有"
    #                },
    #                "safe":0
    #             }
    # # 將JSON轉成str,再轉成bytes格式的消息休
    # data = (bytes(json.dumps(jsonmsg), 'utf-8'))
    # # 使用requests post發送消息
    # requests.post(url, data, verify=False)
    
    class SendMsg(object):
        def __init__(self):
            self.corpid = myCof.get('wechat', 'corpid')
            self.corpsecret = myCof.get('wechat', 'corpsecret')
            self.agentid = myCof.get('wechat', 'agentid')
    
    
        def getToken(self):
            if self.corpid is None or self.corpsecret is None:
                return False, '企業微信相關信息未配置'
            url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=' + self.corpid + '&corpsecret=' + self.corpsecret
            response = requests.get(url)
            res = response.json()
            self.token = res['access_token']
            return True, '企業微信token獲取成功'
    
        def sendMsg(self, msg):
            _isOK, result = self.getToken()
            if _isOK:
                url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=' + self.token
                jsonmsg = {
                       "touser" : "@all",
                       "msgtype" : "text",
                       "agentid" : self.agentid,
                       "text" : {
                           "content" : msg
                       },
                       "safe":0
                    }
                data = (bytes(json.dumps(jsonmsg), 'utf-8'))
                requests.post(url, data, verify=False)
            else:
                print(result)
    
    
    
    
    # wechatMsg = SendMsg()
    # wechatMsg.sendMsg('API接口從無到有')

     

    八、變量參數化模塊-parameteriZation.py

                    前面七小節已經將自動化測試框架的周邊全部弄完了,接下來便開始寫核心模塊了。

                    自動化測試工具有很多變量可以配置,分為兩大類,系統環境變量(在執行之前,人為配置好的變量,執行完還存在),臨時變量(在執行過程參數化的變量,執行完成消失,等待下次參數化)

              1、系統環境變量

                    這個很簡單 ,我們直接寫在配置文件即可,比如我們需要一個賬號與密碼,將其設置為系統環境變量,直接在bascCon.ini里添加

                    

                    然后我們導入 myCon,使用getValue方法就可以取到變量的參數了,

                    

                   這還是第一步,我們需要設計變量的格式,比如 {aaa}, ${aaa},前者使用{}來標識aaa需要參數化,后者用${}來標識aaa需要參數化,

                   那就將${}定義為變量的標識符

                   取變量名

                   那么我們將如何來取變量名呢,比如,${phone},直接使用是取不到手機號碼的,需要將phone取出來。此時我們可以使用正則表達式庫,re來處理

                   正則表達式很強大,規則也很多,在這里不做贅述。re的話是Python的基礎庫,所以可以在菜鳥站上搜索到教程,我們要用到的方法就這兩個

         

          

                   代碼如下與執行結果見下圖

                   

                   將變量參數化   

                   就是在上面代碼里加上獲取配置參數即可,代碼與執行結果見下圖:變量已經參數化了。

     

               2、臨時變量

                     臨時變量,這是個檢驗Python代碼基礎的活兒,面向對象,屬性,setattr  ,getattr等知識點。如果懂了我們只需要在上加幾行代碼就成了。

                    先定義一個空類。不給屬性與方法,在執行代碼的過程中使用setattr給這個空類添加屬性與值,這個屬性即 臨時變量,如果想調用,即可用getattr取屬性的值,進行參數化

                   代碼如下與執行結果如下

                  

        

                  臨時變量參數化

               設置好臨時變量,參數化過程與系統環境變量的差不多,區別是將myCon.getValue(‘par’,  key)  改成getattr(Paramte, key)

               代碼與執行結果如下圖:

              3、代碼封裝

                    parameteriZation.py的代碼段

    import re
    from common.getConfig import myCof
    
    # phone = myCof.getValue('par','phone')
    # print(phone)
    # pwd = myCof.getValue('par', 'pwd')
    # print(pwd)
    #定義一個字符串,里面有兩個變量
    # data = '{PHONE : ${phone}, PASSWORD: ${pwd}}'
    # #定義正則匹配規則
    # ru = r'\${(.*?)}'
    # #循環取變量名稱
    # while re.search(ru, data):
    #     #取值第一個變量
    #     res = re.search(ru, data)
    #     #取出名稱
    #     key = res.group(1)
    #     #取出環境變量
    #     value = myCof.getValue('par', key)
    #     #替換變量
    #     data = re.sub(ru, value, data, 1)
    # #打印替換后的字符串
    # print(data)
    
    # 給臨時變量的空類
    # class Paramete():
    #     pass
    # # 設置臨時變量
    # setattr(Paramete, 'phone', '15381819299')
    # setattr(Paramete, 'pwd', '654321')
    # # 直接調用取值打印
    # # print('直接打印:' + Paramete().phone)
    # # 通過getattr打印
    # # print('getattr打印:' + getattr(Paramete, 'phone'))
    # data = '{PHONE : ${phone}, PASSWORD: ${pwd}}'
    # #定義正則匹配規則
    # ru = r'\${(.*?)}'
    # #循環取變量名稱
    # while re.search(ru, data):
    #     #取值第一個變量
    #     res = re.search(ru, data)
    #     #取出名稱
    #     key = res.group(1)
    #     #取出環境變量
    #     value = getattr(Paramete, key)
    #     #替換變量
    #     data = re.sub(ru, value, data, 1)
    #
    # print(data)
    
    class Paramete:
        pass
    
    def replace_data(data):
        """
        替換變量
        :param data:
        :return:
        """
        ru = r'\${(.*?)}'
        while re.search(ru, data):
            res = re.search(ru, data)
            item = res.group()
            keys = res.group(1)
    
            # 先找系統環境變量,如果有則替換;如果沒有則找臨時變量
            try:
                value = myCof.get('test_data', keys)
            except Exception as e:
                value = getattr(Paramete, keys).encode('utf-8').decode('unicode_escape')
            finally:
                data = re.sub(ru, value, data, 1)
        return data
    
    def analyzing_param(param):
        """
         ${abc}取出abc
        :param param:
        :return:
        """
        ru = r'\${(.*?)}'
        if re.search(ru, param):
            return re.findall(ru, param)[0]
        return param
    
    # print(replace_data('${phone}, ${pwd}'))

     

    九、API請求模塊-sendApirequests.py

           1、requests庫下載

                第三方庫,所以需要用命令:pip install requests   下載

           2、requests庫學習

                requests的請求類型

                常用的請求類型都在下圖

                

                目前主要的請求是get與post

                 requests.get(url=‘請求api的url’, params=‘get請求的參數,可以為空’, headers=‘請求頭,如果接口沒有校驗,可以為空’)

                 requests.post(url=‘請求api的url’, json=‘如果json參數,使用json字段’, data=‘如果是表單格式,使用data參數’, files=‘當數據為文件時,使用file參數’, headers=‘請求頭,如果接口沒有校驗,可以為空’)

                  post里的可以傳json、data、file三種參數,但三個只能傳一個。

     

           3、api請求封裝

                sendApirequest.py代碼段

                這個文件在kemel目錄下面

    class SendApirequests(object):
    
        def __init__(self):
            self.session = requests.session()
    
        def request_Obj(self, method, url, params=None, data=None, json=None, files=None, headers=None,):
            opetype = str.lower(method)
            if opetype == 'get':
                response = requests.get(url=url, params=params, headers=headers)
            elif opetype == 'post':
                response = requests.post(url=url, json=json, data=data, files=files, headers=headers)
            return response

                 封裝代碼調用-get            

     

                 封裝代碼調用-post  

     

    十、公共方法的模塊(核心)-commKeyword.py

          1、公共方法理念

                一個軟件MCV的層次概念

                M是底層,模型層,前面封裝的代碼都是這種模塊,屬於底層代碼 。

                C是中層,控制層,封裝所有底層的接入方法。

                V是高層,會話層,界面也操作提供給用戶使用。

               MVC的區分(分層概念)

               common目錄下的模塊都是M

               kemel目錄下的模塊是C

               解析用例與運行文件是V

               公共方法、主要是分裝所有底層模塊的操作入口,並提供一些特殊性的公共方法

                什么是特殊的公共方法呢?

                 比如:自動生成手機號碼、自動生成身份證號碼,自動生成隨機字符串,自動生成全國各地區號,設置變量、拆分字段,獲取字符串中的指定字段等等。

          2、封裝

                封裝完成公共方法,會與后面的工廠結合起來使用

               commKeyword.py的代碼段

                

    import json
    import jsonpath
    import datetime
    from common.getConfig import myCof
    from common.getCase import Getcase
    from common.operatorDB import OpeartorDB
    from common.parameteriZation import Paramete, analyzing_param, replace_data
    from common.sendApirequest import SendApirequests
    from common.sendEmail import SendMail
    from common.sendMsg import SendMsg
    
    
    class CommKeyword(object):
        def __init__(self):
            self.operatordb = OpeartorDB()
            self.getCase = Getcase()
            self.sendApi = SendApirequests()
            self.sendMail = SendMail()
            self.sedMsg = SendMsg()
    
    
        def get_exceut_case(self, **kwargs):
            """
            獲取當前執行用例
            :return: bl, cases 一參數返回成功與否,二參數用例或失敗原因
            """
            try:
                cases = self.getCase.read_all_excels()
            except Exception as e:
                return False, '獲取用例失敗,原因:' + str(e)
            return True, cases
    
        def get_current_casefile_name(self, **kwargs):
            """
            獲取執行用例文件名稱
            :return: 返回用例文件名稱
            """
            try:
                fileName = myCof.getValue('case', 'testcase')
            except Exception as e:
                return False, '參數中未設置用例文件名稱,請檢查配置文件'
            return True, fileName
    
        def send_api(self, **kwargs):
            """
            發送用例請求 post, get
            :param kwargs:  請求的參數 ,有url,headers,data等
            :return:  bl, cases 一參數返回成功與否,二參數請求結果或失敗原因
            """
            try:
                url = replace_data(kwargs['url'])
                method = kwargs['method']
                if kwargs['headers'] is None:
                    headers = None
                else:
                    _isOk, result = self.format_headers(replace_data(kwargs['headers']))
                    if _isOk:
                        headers = result
                    else:
                        return _isOk, result
    
                if kwargs['data'] is not None:
                    try:
                        jsondata = json.loads(replace_data(kwargs['data']))
                        data = None
                    except ValueError:
                        data = replace_data(kwargs['data'])
                        jsondata = None
                else:
                    data = None
                    jsondata = None
                response = self.sendApi.request_Obj(method=method, url=url, json=jsondata, data=data, headers=headers)
            except Exception as e:
                return False, '發送請求失敗' + str(e)
            return True, response
    
    
    
    
    
        def set_sheet_dict(self):
            """
            :return: excl文件里面的sheet頁信息
            """
            xlsx = Getcase(myCof.get('excel', 'casename'))
            sh_dict = xlsx.sheet_count()
            setattr(Paramete, 'sheetdict', sh_dict)
            sheetdict = getattr(Paramete, 'sheetdict')
            return sheetdict
    
    
        def set_common_param(self, key, value):
            """
            :param key:  公共變量名
            :param value: 參數
            :return:
            """
            setattr(Paramete, key, value)
    
        def get_commom_param(self, key):
            """
            :param key: 公共變量名
            :return: 取變量值
            """
            return getattr(Paramete, key)
    
    
        def get_current_sheet_name(self):
            """
            :return: 返回當前執行用例的sheet頁名稱
            """
            sh_index = self.get_commom_param('sheetindex')
            sh_dict = self.get_commom_param('sheetdict')
            for sh in sh_dict:
                if sh.title().find(str(sh_index)) != -1:
                    sheet_name = sh_dict[sh.title().lower()]
            return sheet_name
    
        def get_json_value_as_key(self, *args, **kwargs):
            """
            得到json中key對應的value,存變量param
            默認傳的參數為:
            result:用來接收結果的變量
            method:調用的方法 ,帶不帶${    } 都行
            param_x:參數,數量不限。格式可為${    }會替換為已存在的數據
            """
            try:
                param = kwargs['result']
                jsonstr = kwargs['param_1']
                key = kwargs['param_2']
            except KeyError:
                return False, '方法缺少參數,執行失敗'
    
            param = analyzing_param(param)
            jsonstr = replace_data(jsonstr)
            key = replace_data(key)
    
            if param is None or jsonstr is None or key is None:
                return False, '傳入的參數為空,執行失敗'
            try:
                result = json.loads(jsonstr)
            except Exception:
                return False, '傳入字典參數格式錯誤,執行失敗'
            key = '$..' + key
            try:
                value = str(jsonpath.jsonpath(result, key)[0])
            except Exception:
                return False, '字典中[' + jsonstr + ']沒有鍵[' + key + '], 執行失敗'
            setattr(Paramete, param, value)
            return True, ' 已經取得[' + value + ']==>[${' + param + '}]'
    
    
    
        def format_headers(self, param):
            """
            格式化請求頭
            :param param:excel里讀出出來的header,是從瀏覽器f12里直接copy的
            :return:
            """
            if param is None:
                return False, 'Headers為空'
            list_header = param.split('\n')
            headers = {}
    
            for li in list_header:
                buff = li.split(':')
                try:
                    headers[buff[0]] = buff[1]
                except IndexError:
                    return False, 'Headers格式不對'
            return True, headers
    
    
        def set_variable(self, **kwargs):
            """
            設置變量
            :param kwargs:
            :return:
            """
            try:
                var = kwargs['result']
                param = kwargs['param_1']
            except KeyError:
                return False, '方法缺少參數,執行失敗'
            if var is None or param is None:
                return False, '傳入的參數為空,執行失敗'
            setattr(Paramete, var, param)
            return True, ' 已經設置變量[' + param + ']==>[${' + var + '}]'
    
    
        def execut_sql(self, **kwargs):
            """
            執行SQL
            :param kwargs:
            :return:
            """
            try:
                sql = kwargs['param_1']
            except KeyError:
                return False, '方法缺少參數,執行失敗'
            try:
                var = kwargs['result']
                par = kwargs['param_2']
            except Exception:
                var = None
            isOK, result = self.operatordb.excetSql(sql)
            if isOK and var is not None:
                data = result[par]
                setattr(Paramete, var, data)
                return True, '執行SQL:[' + sql + ']成功,取得' + par + '的數據[' + data + ']==>[${' + var + '}]'
            elif isOK and var is None:
                return True, '執行SQL:[' + sql + ']成功'
            elif isOK is False:
                return isOK, result
    
    
        def send_email(self):
            """
            發送郵件
            :return:
            """
            return self.sendMail.send_email()
    
    
        def send_msg(self, **kwargs):
            """
            發送消息
            :param kwargs:
            :return:
            """
            title = kwargs['title']
            url = kwargs['url']
            code = kwargs['code']
            result = kwargs['result'].encode('utf-8').decode('unicode_escape')
            nowTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')  # 現在
            msg = nowTime + '\n用例名稱:' + title + '\n請求:' + url + '\n響應碼:' + code + '\n響應信息:' + result
            self.sedMsg.sendMsg(msg)
    
    
    # header = 'Access-Control-Allow-Credentials: true\nAccess-Control-Allow-Origin: http://test-hcz-static.pingan.com.cn\naccessToken: 8b1f056249134c4f9fb7b573b25ce08c'
    # _isOK, headers = format_headers(header)
    # print(headers, type(headers))
    

                

     

    十一、工廠封裝(核心)-methodFactoy.py

           1、工廠的邏輯

                 工廠是公共方法調用入口。

                 有人傳入關鍵的文字,而工廠會找到關鍵文字對應的公共方法,執行方法得到結果,最后返回給調用工廠的人。

                 那我們如何通過文字找方法呢?

                 需要用到配置文件,我們在配置文件將前面小節封裝的公共方法逐一配置好,

                 格式:文字=方法名稱

                 如下圖:

                 

                 代碼實現,先通過配置文件得到公共方法的名稱 ,然后使用getattr方法在公共模塊對象上找到公共方法,然后執行方法,可以得到想要的結果。

                 如下圖

                 

    如果是在用例里執行,以這種格式寫

    打印結果與報告展示

     

    這個需要將后面的封閉與用例解析寫完才能看到的效果,加油 

     

     

           2、封裝

                

                methodFactory.py的代碼段

    from common.getConfig import myCof
    from kemel.commKeyword import CommKeyword
    
    # #初始化公共方法模塊
    # comKey = CommKeyword()
    # #獲取所有公共方法配置參數
    # comKW = dict(myCof.items('commkey'))
    # #獲取 取用例方法名稱
    # method_Name = comKW['獲取當前執行用例']
    # #通過getattr 獲取公共方法模塊的對應的模塊
    # func = getattr(comKey, method_Name, None)
    # #執行前面獲取的公共方法得到用例
    # cases = func(aa=None)
    # #打印用例
    # print(cases)
    
    class MethodFactory(object):
    
        def __init__(self):
            self.comKey = CommKeyword()
            self.comKW = dict(myCof.items('commkey'))
    
    
        def method_factory(self, **kwargs):
            """
            用例公共方法工廠
            默認傳的參數為:
            result:用來接收結果的變量,格式可為${abc}
            method:調用的方法,這里設計方法都使用中文
            param_x:參數,數量不限。格式可為${abc}會替換為已存在的數據
            """
    
            if kwargs.__len__() > 0:
                try:
                    kwargs['method']
                except KeyError:
                    return False, 'keyword:用例[method]字段方法沒參數為空.'
                try:
                    method = self.comKW[str(kwargs['method']).lower()]
                except KeyError:
                    return False, 'keyword:方法[' + kwargs['method'] + '] 不存在,或未配置.'
            else:
                return False, '沒有傳參'
            try:
                func = getattr(self.comKey, method, None)
                _isOk, reselt = func(**kwargs)
                return _isOk, reselt
            except Exception as e:
                return False, 'keyword:執行失敗,估計不存在,異常:' + str(e)
    
    
    
    
    
    # fac = MethodFactory()
    # print(fac.method_factory(method='獲取當前用例文件名稱'))
    
    
    
    
    

     

     

    十二、解析用例(核心)-testCase.py

           1、詳情講解

                python 自動了一個單元測試框架unittest,用來做自動化測試是絕好的。

               先引用一段理論:

    --------------------------------------------

    做過自動化測試的同學應該都知道python中的unittest框架,它是python自帶的一套測試框架,學習起來也相對較容易,unittest框架最核心的四個概念:

    ​  test case:就是我們的測試用例,unittest中提供了一個基本類TestCase,可以用來創建新的測試用例,一個TestCase的實例就是一個測試用例;unittest中測試用例方法都是以test開頭的,且執行順序會按照方法名的ASCII值排序。

    ​  test fixure:測試夾具,用於測試用例環境的搭建和銷毀。即用例測試前准備環境的搭建(SetUp前置條件),測試后環境的還原(TearDown后置條件),比如測試前需要登錄獲取token等就是測試用例需要的環境,運行完后執行下一個用例前需要還原環境,以免影響下一條用例的測試結果。

    ​  test suite:測試套件,用來把需要一起執行的測試用例集中放到一塊執行,相當於一個籃子。我們可以使用TestLoader來加載測試用例到測試套件中。

    ​  test runner:用來執行測試用例的,並返回測試用例的執行結果。它還可以用圖形或者文本接口,把返回的測試結果更形象的展現出來,如:HTMLTestRunner。

    --------------------------------------------

               自動化測試流程:是基於unittest中TestCase + ddt data 模式成自動化用例集(俗稱數據驅動)。而后被unittest中的test suite套件將用例集中管理起來,最后使用unittest中的test runner將集中起來的用例執行,生成測試報告

                解析用例。就是數據驅動這一段。已經封裝好在testCase.py中了可以自行看代碼與注釋學習

     

           2、封裝

                testCase.py代碼段

    import unittest
    import json
    from library.ddt import ddt, data
    from common.log import mylog
    from kemel.methodFactory import MethodFactory
    
    isOK = True
    e = Exception()
    
    @ddt      #引用數據驅動裝飾器
    class TestCase(unittest.TestCase):
        metFac = MethodFactory()     #初始化工廠類
        isOK, cases = metFac.method_factory(method='獲取當前執行用例')
        isOK, fileName = metFac.method_factory(method='獲取當前用例文件名稱')
        if isOK is False:
            mylog.error('獲取用例失敗')
            quit()
        
        #調用工廠公共方法入口
        def _opear_keyword(self, **kwargs):    
            return self.metFac.method_factory(**kwargs)
    
        #斷言方法
        def _assert_res_expr(self, rules, reponse, expr):
            """
            斷言方法
            :param rules:結果包含、結果等於、結果狀態
            :param res:
            :param expr:
            :return:
            """
            try:
                res = reponse.json()
            except Exception:
                res = reponse.text
    
            headers = reponse.headers
            code = str(reponse.status_code)
            _reason = 'success'
            if rules == '結果包含':
                if type(expr) is str:
                    res = json.dumps(res, ensure_ascii=False)
                    print_result = json.dumps(json.loads(res), sort_keys=True, indent=2, ensure_ascii=False)
                else:
                    print_result = res
                try:
                    self.assertIn(expr, res)
                except AssertionError as e:
                    _isOk = False
                    _reason = '結果:\n【' + print_result + '】\n  不包含校驗值:\n  【' + expr + '】'
                else:
                    _isOk = True
                    _reason = '結果:\n【' + print_result + '】\n  包含有校驗值:\n  【' + expr + '】'
            elif rules == '結果等於':
                if type(expr) is str:
                    res = json.dumps(res, ensure_ascii=False)
                    print_result = json.dumps(json.loads(res), sort_keys=True, indent=2, ensure_ascii=False)
                else:
                    print_result = res
                try:
                    self.assertEqual(expr, res)
                except AssertionError as e:
                    _isOk = False
                    _reason = '結果:\n【' + res + '】\n  不等於校驗值:\n  【' + expr + '】'
                else:
                    _isOk = True
                    _reason = '結果:\n【' + res + '】\n  等於校驗值:\n  【' + expr + '】'
            elif rules == '結果狀態':
                try:
                    self.assertEqual(expr, code)
                except AssertionError as e:
                    _isOk = False
                    _reason = '結果:\n【' + code + '】\n  不等於校驗值:\n  【' + expr + '】'
                else:
                    _isOk = True
                    _reason = '結果:\n【' + code + '】\n  等於校驗值:\n  【' + expr + '】'
            elif rules == '頭部包含':
                if type(expr) is str:
                    headers = json.dumps(headers, ensure_ascii=False)
                    print_header = json.dumps(json.loads(headers), sort_keys=True, indent=2, ensure_ascii=False)
                else:
                    print_header = headers
                try:
                    self.assertIn(expr, headers)
                except AssertionError as e:
                    _isOk = False
                    _reason = '結果頭:\n【' + print_header + '】\n  不包含校驗值:\n 【' + expr + '】'
                else:
                    _isOk = True
                    _reason = '結果頭:\n【' + print_header + '】\n  包含有校驗值:\n 【' + expr + '】'
            elif rules == '頭部等於':
                if type(expr) is str:
                    headers = json.dumps(headers, ensure_ascii=False)
                    print_header = json.dumps(json.loads(headers), sort_keys=True, indent=2, ensure_ascii=False)
                else:
                    print_header = headers
                try:
                    self.assertEqual(expr, headers)
                except AssertionError as e:
                    _isOk = False
                    _reason = '結果頭:\n【' + print_header + '】\n  不等於校驗值:\n  【' + expr + '】'
                else:
                    _isOk = True
                    _reason = '結果頭:\n【' + print_header + '】\n  等於校驗值:\n  【' + expr + '】'
            return _isOk, _reason
    
        
        #打印用例信息與執行結果,因為是TestCase,最終它們將展示到測試報告中,所以設計輸出格式,讓報告美美達
        def postPinrt(self, **case):
            if case['interface'] is not None:
                print('\n------------------------------------------------------------------\n')
            print('接口:【' + case['interface'] + '】')
            if case['method'] is not None:
                print('類型:【' + case['method'] + '】')
            if case['data'] is not None:
                print('參數:【' + case['data'] + '】')
            if 'get' == str.lower(case['method']):
                if '?' in str(case['url']):
                    url = str(case['url'])
                    a, param = url.split('?')
                    if param is not None:
                        print('參數:【')
                        datalist = str(param).split('&')
                        for data in datalist:
                            print(data)
                        print('】')
                else:
                    print('【沒帶參數】')
            print('\n------------------------------------------------------------------\n')
    
    
    
    
        @data(*cases)     #,數據驅動裝飾器,將用例list中的元素出來,將元素傳遞給test_audit的case中
        def test_audit(self, case):
            _isOk = True
            #如果interface為commfun,將調用對應的公共方法
            if case['interface'] == 'commfun':
                """
                如果接口是公共方法,那么字段如下
                method:公共方法名
                title: 返回接果
                url:參數
                data:參數 ...暫時四個參數
                """
                _isOk, _strLog = self._opear_keyword(method=case['method'],
                                                     result=case['title'],
                                                     param_1=case['url'],
                                                     param_2=case['headers'],
                                                     param_3=case['data'],
                                                     param_4=case['validaterules'])
            else:
                rows = case['case_id'] + 1
                title = case['title']
                expect = str(case['expected'])
                #發送請求,用例文件里interface不等於commfun,method為post或get的將被執行
                _isOK, result = self.metFac.method_factory(**case)
                if _isOk:
                    response = result
                    code = str(response.status_code)
                    try:
                        res = json.dumps(response.json())
                        self.metFac.method_factory(method='設置變量', result='response', param_1=response)    #返回json存
                    except ValueError:
                        res = response.text
                        self.metFac.method_factory(method='設置變量', result='response', param_1=res)     #返回html 或xml、 txt存
    
                    if case['validaterules'] is None:
                        _isOk = True
                        _strLog = '用例[' + str(case['case_id']) + ']:[' + title + ']執行完成.'
                    else:
                        rules = case['validaterules']
                        _isOk, _reason = self._assert_res_expr(rules, response, expect)
                        if _isOk:
                            _strLog = '用例[' + str(case['case_id']) + ']:[' + title + ']執行通過. \n 校驗結果:\n' + _reason
                        else:
                            _strLog = "用例[" + str(case['case_id']) + ']:[' + title + ']執行不通過.\n 原因:\n' + _reason
                            #報錯的接口,給企業微信發送信息
                            self.metFac.method_factory(title=title, method='發送消息', api=case['interface'], url=case['url'], code=code, result=res)
                else:
                    _strLog = "用例[" + str(case['case_id']) + ']:[' + title + ']執行不通過. \n 原因:\ n' + result
    
            if _isOk:
                mylog.info(_strLog)
                print(_strLog)
                self.postPinrt(**case)
    
            else:
                mylog.error(_strLog)
                print(_strLog)
                self.postPinrt(**case)
                raise
    
    
    
    
    

     

    十三、最后的運行文件-testRun.py

            1、代碼封裝

                 testRun.py代碼段

                寫完這個文件的代碼,再然后按照第三小節的   ’讀取用例模塊-getCase.py‘ 里面的規則設計測試用例,然后放支data文件目錄里,將配置文件里的用例文件名稱配置好,然后執行自動化測試了

    
    import unittest
    import os
    from common.initPath import CASEDIR, REPORTDIR
    from kemel.methodFactory import MethodFactory
    from library.HTMLTestRunnerNew import HTMLTestRunner
    
    
    
    class TestRun(object):
        metFac = MethodFactory()
        def __init__(self):
            self.suit = unittest.TestSuite()
            load = unittest.TestLoader()
            self.suit.addTest(load.discover(CASEDIR))
    
            self.runner = HTMLTestRunner(
                stream=open(os.path.join(REPORTDIR, 'report.html'), 'wb'),
                title='接口自動化測試報告',
                description='代替手動冒煙、手動回歸,做更精准的測試',
                tester='HunWei'
            )
    
        def excute(self):
            self.runner.run(self.suit)
            self.metFac.method_factory(method='發送郵件')
    
    
    
    if __name__=='__main__':
        run = TestRun()
        run.excute()
    # from kemel.methodFactory import MethodFactory
    # abc = MethodFactory()
    # isOK, cases = abc.method_factory(method='獲取當前執行用例')
    # print(cases)
    
    

                執行后的測試報告如下:

     

     

       

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM