1. 程式人生 > 其它 >Python從無到有搭建介面(API)自動化測試框架

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)
    
    

                執行後的測試報告如下: