pytest+requests+Python3.7+yaml+Allure+Jenkins+docker實現接口自動化測試

接口自動化測試框架(用例自動生成)

項目說明

  • 本框架是一套基於pytest+requests+Python3.7+yaml+Allure+Jenkins+docker而設計的數據驅動接口自動化測試框架,pytest 做爲執行器,本框架無需你使用代碼編寫用例,那你可能會擔憂萬一有接口之間相互依賴,或者說須要登入的token等之類的接口,該如何編寫用例呢,在這裏告訴大家本框架已經完美解決此問題,全部的一切將在yaml中進行!!本框架實現了在yaml中進行接口用例編寫,接口依賴關聯,接口斷言,自定義測試用例運行順序,還有很重要的一點,實現了類jmeter函數助手的功能,譬如生成MD五、SHA一、隨機定長字符串、時間戳等,只須要你在yaml中使用特殊的寫法$Function(arg)$,就可以使用這些函數啦,此外在測試執行過程當中,還能夠 對失敗用例進行屢次重試,其重試次數和重試時間間隔可自定義;並且能夠根據實際須要擴展接口協議,目前已支持http接口和webservice接口

技術棧

  • requests
  • suds-py3
  • Allure
  • pytest
  • pytest-html
  • yaml
  • logging
  • Jenkins
  • docker
  • 函數助手

環境部署

  • 命令行窗口執行pip install -r requirements.txt 安裝工程所依賴的庫文件html

  • 解壓allure-commandline-2.12.1.zip到任意目錄中java

  • 打開\allure-2.12.1\bin文件夾,會看到allure.bat文件,將此路徑添加到系統環境變量path下,這樣cmd任意目錄都能執行了web

  • 在cmd下執行 allure --version ,返回版本信息,allure即安裝成功正則表達式

  • 進入 \Lib\site-packages\allure 下面的utils文件,修改爲如下代碼:docker

    for suitable_name in suitable_names:
          # markers.append(item.get_marker(suitable_name))
          markers.append(item.get_closest_marker(suitable_name))

目的是解決pytest運行產生的如下錯誤:
_pytest.warning_types.RemovedInPytest4Warning: MarkInfo objects are deprecated as they contain merged marks which are hard to deal with correctly.
shell

框架流程圖與目錄結構圖及相關說明

一、框架流程圖以下

二、代碼目錄結構圖以下

目錄結構說明

  • config ===========> 配置文件
  • common ===========> 公共方法封裝,工具類等
  • pytest.ini ==========> pytest的主配置文件,能夠改變pytest的默認行爲,如運行方式,默認執行用例路徑,用例收集規則,定義標記等
  • log ==========> 日誌文件
  • report ==========> 測試報告
  • tests ===========> 待測試相關文件,好比測試用例和用例數據等
  • conftest.py ============> 存放測試執行的一些fixture配置,實現環境初始化、數據共享以及環境還原等
  • requirements.txt ============> 相關依賴包文件
  • Main.py =============> 測試用例總執行器
  • RunTest_windows.bat ============> 測試啓動按鈕

conftest.py配置說明

  • conftest.py文件名字是固定的,不能夠作任何修改
  • 不須要import導入conftest.py,pytest用例會自動識別該文件,若conftest.py文件放在根目錄下,那麼conftest.py做用於整個目錄,全局調用
  • 在不一樣的測試子目錄也能夠放conftest.py,其做用範圍只在該層級以及如下目錄生效
  • 全部目錄內的測試文件運行前都會先執行該目錄下所包含的conftest.py文件
  • conftest.py文件不能被其餘文件導入

conftest.py與fixture結合

conftest文件實際應用中須要結合fixture來使用,以下數據庫

  • conftest中fixture的scope參數爲session時,那麼整個測試在執行前會只執行一次該fixture
  • conftest中fixture的scope參數爲module時,那麼每個測試文件執行前都會執行一次conftest文件中的fixture
  • conftest中fixture的scope參數爲class時,那麼每個測試文件中的測試類執行前都會執行一次conftest文件中的fixture
  • conftest中fixture的scope參數爲function時,那麼全部文件的測試用例執行前都會執行一次conftest文件中的fixture

conftest應用場景

  • 測試中需共用到的token
  • 測試中需共用到的測試用例數據
  • 測試中需共用到的配置信息
  • 結合 yield 語句,進行運行前環境的初始化和運行結束後環境的清理工做,yield前面的語句至關於unitest中的setup動做,yield後面的語句至關於unitest中的teardown動做,無論測試結果如何,yield後面的語句都會被執行。
  • 當fixture超出範圍時(即fixture返回值後,仍有後續操做),經過使用yield語句而不是return,來將值返回(由於return後,說明該函數/方法已結束,return後續的代碼不會被執行),以下:

@pytest.fixture(scope="module")
def smtpConnection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp_connection  # 返回 fixture 值smtp_connection
    print("teardown smtp")
    smtp_connection.close()

不管測試的異常狀態如何,print和close()語句將在模塊中的最後一個測試完成執行時執行。json

  • 可使用with語句無縫地使用yield語法(with語句會自動釋放資源)

@pytest.fixture(scope="module")
def smtpConnection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: 
        yield smtp_connection  # 返回smtp_connection對象值

測試結束後, 鏈接將關閉,由於當with語句結束時,smtp_connection對象會自動關閉。windows

關聯詳解

  • 公共關聯池:意思就是你能夠存儲接口的響應值到參數池中,以便後續接口使用;同時也能夠在測試執行前,製造一些公共關聯值,存儲到參數池中,供全部的接口使用;
  • 在yaml測試用例中,可經過填寫關聯鍵relevance提取響應字段的鍵值對到參數池中;當提取單個關聯值時,關聯鍵relevance的值爲字符串,形如relevance: positon;當提取多個關聯值時,關聯鍵relevance的值爲列表,同時也可提取響應信息中嵌套字典裏的鍵值對;
  • 引用已經存儲的關聯值:在下個接口入參中使用形如 ${key}$ 的格式,便可提取參數池中的key對應的value,固然你必須保證關聯池中已經存儲過該key。

函數助手詳解

  • 說明:函數助手是來自Jmeter的一個概念,有了它意味着你能在yaml測試用例或者其餘配置文件中使用某些函數動態的生成某些數據,好比隨機定長字符、隨機定長整型數據、隨機浮點型數據、時間戳(10位和13位)、md5加密、SHA一、SHA25六、AES加解密等等,引用的格式爲 $Function(arg)$
  • 目前支持的函數助手:
  • $MD5(arg)$ =========》 md5加密字符串,arg爲待加密的字符串,可傳入多個字符串拼接後加密,多個字符串之間用逗號隔開,形如$MD5(asd, we57hk)$
  • $SHA1(arg)$ ==========》 SHA1加密字符串,arg爲待加密的字符串,可傳入多個字符串拼接後加密,多個字符串之間用逗號隔開
  • $SHA256(arg)$ ==========》 SHA256加密字符串,arg爲待加密的字符串,可傳入多個字符串拼接後加密,多個字符串之間用逗號隔開
  • $DES(arg, key)$ ==========》 DES加密字符串,arg爲待加密的字符串
  • $AES(arg, key, vi)$ ==========》 AES加密字符串,arg爲待加密的字符串
  • $RandomString(length)$ =========》 生成定長的隨機字符串(含數字或字母),length爲字符串長度
  • $GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$ =========》 生成定長的時間戳,time_type=now表示獲取當前時間,layout=13timestamp表示時間戳位數爲13位,unit爲間隔的時間差

代碼設計與功能說明

一、定義運行配置文件 runConfig.yml

該文件主要控制測試的執行方式、模塊的功能開關、測試用例的篩選、郵件的配置以及日誌的配置,具體以下:api


runConfig.yml配置信息

# 自動生成測試用例開關,0 -關, 1 -開,根據接口數據自動生成測試用例和單接口執行腳本; 2 -開,根據手工編寫的測試用例,自動生成單接口執行腳本
writeCase_switch: 0
# 本次自動生成的測試用例歸屬的功能模塊(項目名稱/功能模塊)好比: /icmc/pushes ;若不填,則默認不歸類
ProjectAndFunction_path: /icmc/pushes
# 掃描用例路徑(相對於TestCases的相對路徑),以生成執行腳本;若不填,則默認掃描全部的測試用例(只有自動生成測試用例開關爲 2 時,此字段纔有效),如 /icmc/pushes
scan_path:
# 執行接口測試開關,0 -關, 1 -開
runTest_switch: 1

# 從上往下逐級篩選
# 待執行項目 (可用表達式:not、and、or)(項目名最好惟一,若多個項目或測試名的前綴或後綴相同,則也會被檢測到;檢測規則爲「包含」)
Project: tests

# 待執行接口,可運行單獨接口(填接口名),可運行全部接口(None或者空字符串時,即不填),挑選多接口運行可用表達式:not、and、or ,如 parkinside or GetToken or company
markers:

# 本次測試需排除的產品版本(列表),不填,則默認不排除
product_version:

# 本次測試執行的用例等級(列表),不填,則默認執行全部用例等級;可選['blocker', 'critical', 'normal', 'minor', 'trivial']
case_level:
- blocker
- critical
- normal
- minor

# isRun開關,0 -關, 1 -開 ;關閉時,則用例中的is_run字段無效,即會同時執行is_run爲 False 的測試用例
isRun_switch: 1

# 用例運行間隔時間(s)
run_interval: 0

# 本輪測試最大容許失敗數,達到最大失敗數時,則會當即結束當前測試
maxfail: 20

# 測試結束後,顯示執行最慢用例數(如:3,表示顯示最慢的三條用例及持續時間)
slowestNum: 3

# 失敗重試次數,0表示不重試
reruns: 1
# 失敗重試間隔時間(s)
reruns_delay: 0.1

#發送測試報告郵件開關, 0 -關, 1 -開 
emailSwitch: 0
#郵件配置
#發件郵箱
smtp_server: smtp.126.com
server_username:XXXX@126.com
server_pwd: XXXXX
#收件人(列表)
msg_to:
- XXX@163.com
- XXX@qq.com

#郵件主題
msg_subject: '[XX項目][測試環境-develop][jira號][接口自動化測試報告]'

#日誌級別(字典),由高到低: CRITICAL 、 ERROR 、 WARNING 、 INFO 、 DEBUG
log:
    backup: 5
    console_level: INFO           #控制檯日誌級別
    file_level: DEBUG              #文件日誌級別
    pattern: '%(asctime)s - %(filename)s [line:%(lineno)2d] - %(levelname)s: %(message)s'

二、接口配置文件 apiConfig.ini


[host]
host = 127.0.0.1:12306
MobileCodeWS_host = ws.webxml.com.cn
WeatherWebService_host = www.webxml.com.cn

[header]
header1 = {"Content-Type": "application/json"}
header2 = {"Content-Type": "application/json;charset=UTF-8"}
header3 = {"Content-Type": "application/json", "description": "$RandomString(10)$",
            "timestamp": "$GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$", "sign": "$SHA1(${description}$, ${timestamp}$)$"}

[MySqlDB]
host = localhost
port = 3306
user = root
pwd = root
db = course
charset = utf8
  • 能夠針對不一樣的項目,配置不一樣的host、header等,經過不一樣的命名區分,如header一、header2,在yaml測試用例文件中,經過變量名引用便可,好比${host}$${header1}$
  • 在該接口配置文件裏的字段值,能夠調用函數助手的功能,引用相關函數,好比header3,在其字段值裏即引用了函數RandomStringtimestamp產生須要的值,並將值拼接在一塊兒,而後再用加密函數SHA1加密後,傳給sign。

三、測試用例的設計

測試用例以yaml格式的文件保存,簡潔優雅,表達力又強,用例直接反映了接口的定義、請求的數據以及指望的結果,且將測試用例中的公共部分提取出來,平時只需維護測試數據和指望結果,維護成本低。
yaml測試用例的數據格式以下:

  • http類型接口

# 用例基本信息
test_info:
      # 用例標題,在報告中做爲一級目錄顯示,用接口路徑倒數第二個字段名做爲標題
      title: parkinside
      # 用例所屬產品版本,不填則爲None
      product_version: icm_v1.0
      # 用例等級,優先級,包含blocker, critical, normal, minor,trivial幾個不一樣的等級
      case_level: blocker
      # 請求的域名,可寫死,也可寫成模板關聯host配置文件,也可寫在用例中
      host: ${host}$
      # 請求地址 選填(此處不填,每條用例必填)
      address: /${api}$
      # 請求頭 選填(此處不填,每條用例必填,若有的話)
      headers: ${header1}$
      # 請求協議
      http_type: http
      # 請求類型
      request_type: POST
      # 參數類型
      parameter_type: json
      # 是否須要獲取cookie
      cookies: False
      # 是否爲上傳文件的接口
      file: False
      # 超時時間
      timeout: 20
      # 運行順序,當前接口在本輪測試中的執行次序,1表示第一個運行,-1表示最後一個運行
      run_order: 1

# 前置條件,case以前需關聯的接口,與test_case相似,關聯接口可寫多個 
premise:
      - test_name: 獲取token    # 必填
        info: 正常獲取token值  # 選填
        address: /GetToken   # 請求接口
        http_type: http             # 請求協議
        request_type: GET          # 請求方式
        parameter_type:    # 參數類型,默認爲params類型
        headers: {}                # 請求頭
        timeout: 10                 # 超時時間
        parameter:                 # 可填實際傳遞參數,若參數過多,可保存在相應的參數文件中,用test_name做爲索引
              username: "admin"
              password: "123456"    
        file: False                 # 是否上傳文件,默認false,若上傳文件接口,此處爲文件相對路徑 bool or string
        relevance:  # 關聯的鍵 list or string ;string時,直接寫在後面便可;可提取多個關聯值,以列表形式,此處提取的關聯值可用於本模塊的全部用例

# 測試用例 
test_case:
    - test_name: parkinside_1
      # 用例ID,第一條用例必填,從1開始遞增
      case_id: 1
      # 是否運行用例,不運行爲 False ,空值或其它值則運行
      is_run:
      # 用例描述
      info: parkinside test
      # 參數保存在單獨文件中時,可經過文件路徑引入參數
      parameter: data_parkinside.json

      # 校驗列表(指望結果)
      check:
            expected_result: result_parkinside.json   # 指望結果保存在單獨文件中時,可經過文件路徑引入
            check_type: json                          # 校驗類型,這裏爲json校驗
            expected_code: 200                        # 指望狀態碼

      # 關聯做用範圍,True表示全局關聯,其餘值或者爲空表示本模塊關聯
      global_relevance:
      # 關聯鍵,此處提取的關聯值可用於本模塊後續的全部用例
      relevance:
            - userID
            - vpl

    - test_name: parkinside_2
      # 第二條用例
      case_id: 2
      is_run:
      info: parkinside
      # 請求的域名
      host: 127.0.0.1:12306
      # 請求協議
      http_type: http
      # 請求類型
      request_type: POST
      # 參數類型
      parameter_type: json
      # 請求地址
      address: /parkinside
      # 請求頭
      headers:
            Content-Type: application/json
      # 請求參數
      parameter:
            sign: ${sign}$    # 經過變量引用關聯值
            vpl: AJ3585
      # 是否須要獲取cookie
      cookies: False
      # 是否爲上傳文件的接口
      file: False
      # 超時時間
      timeout: 20

      # 校驗列表
      check:
            check_type: Regular_check   #正則校驗,多項匹配
            expected_code: 200
            expected_result:
                  - '"username": "wuya'      
                  - '"Parking_time_long": "20小時18分鐘"'
                  - '"userID": 22'
                  - '"Parking fee": "20\$"'

      global_relevance:
      # 關聯鍵
      relevance:

    - test_name: parkinside_3
      # 第三條用例
      case_id: 3
      # 是否運行用例
      is_run:
      # 用例描述
      info: parkinside
      # 請求參數
      parameter:
            vpl: ${vpl}$
            userID: ${userID}$

      # 校驗列表
      check:
            expected_result: result_parkinside.json     # 指望結果保存在單獨文件中時,可經過文件路徑引入
            check_type: only_check_status
            expected_code: 400

      global_relevance:
      # 關聯鍵
      relevance:
  • webservice類型接口1

# 用例基本信息
test_info:
      # 用例標題
      title: MobileCodeWS_getMobileCodeInfo
      # 用例所屬產品版本
      product_version: icm_v5.0
      # 用例等級
      case_level: normal
      # 請求的域名,可寫死,也可寫成模板關聯host配置文件,也可寫在用例中
      host: ${MobileCodeWS_host}$
      # 請求地址 選填(此處不填,每條用例必填)
      address: /WebServices/MobileCodeWS.asmx?wsdl
      # 請求頭 選填(此處不填,每條用例必填,若有的話)
      headers:
      # 請求協議
      http_type: http
      # 請求類型
      request_type: SOAP
      # webservice接口裏的函數名
      function_name: getMobileCodeInfo
      # 參數類型(get請求通常爲params,該值可不填)
      parameter_type:
      # 是否須要獲取cookie
      cookies: False
      # 是否爲上傳文件的接口
      file: False
      # 超時時間(s),SOAP默認超時鏈接爲90s
      timeout: 100
      # 運行順序
      run_order:

# 前置條件,case以前需關聯的接口
premise:

# 測試用例
test_case:
    - test_name: getMobileCodeInfo_1
      # 用例ID
      case_id: 1
      is_run:
      # 用例描述
      info: getMobileCodeInfo test
      # 請求參數
      parameter:
	      mobileCode: "18300000000"
          userID: ""

      # 校驗列表
      check:
          check_type: equal
          expected_result: result_getMobileCodeInfo.json
          expected_code:

      global_relevance:
      # 關聯鍵
      relevance:

    - test_name: getMobileCodeInfo_2
      case_id: 2
      is_run:
      info: getMobileCodeInfo test
      # 請求參數
      parameter:
          mobileCode: "18300000000"
          userID: ""

      # 校驗列表
      check:
          check_type: equal
          expected_result: result_getMobileCodeInfo.json
          expected_code:

      global_relevance:
      # 關聯鍵
      relevance:

    - test_name: getMobileCodeInfo_3
      case_id: 3
      is_run: 
      info: getMobileCodeInfo test
      # 請求參數
      parameter:
          mobileCode: "18300000000"
          userID: ""

      # 校驗列表
      check:
        check_type: Regular
        expected_result:
            - '18300000000:廣東'
            - '深圳 廣東移動全球通卡'
        expected_code:

      global_relevance:
      # 關聯鍵
      relevance:

    - test_name: getMobileCodeInfo_4
      case_id: 4
      is_run:
      info: getMobileCodeInfo test
      parameter:
          mobileCode: "18300000000"
          userID: ""

      # 校驗列表 
      check:
          check_type: no_check
          expected_code:
          expected_result:

      global_relevance:
      # 關聯鍵
      relevance:
  • webservice類型接口2

# 用例基本信息
test_info:
      # 用例標題
      title: MobileCodeWS_getMobileCodeInfo
      # 用例所屬產品版本
      product_version: icm_v5.0
      # 用例等級
      case_level: normal
      # 請求的域名,可寫死,也可寫成模板關聯host配置文件,也可寫在用例中
      host: ${WeatherWebService_host}$
      # 請求地址 選填(此處不填,每條用例必填)
      address: /WebServices/WeatherWebService.asmx?wsdl
      # 請求過濾地址
      filter_address: http://WebXml.com.cn/
      # 請求頭 選填(此處不填,每條用例必填,若有的話)
      headers:
      # 請求協議
      http_type: http
      # 請求類型
      request_type: soap_with_filter
      # webservice接口裏的函數名
      function_name: getSupportCity
      # 參數類型
      parameter_type:
      # 是否須要獲取cookie
      cookies: False
      # 是否爲上傳文件的接口
      file: False
      # 超時時間(s),SOAP默認超時鏈接爲90s
      timeout: 100
      # 運行順序
      run_order:

# 前置條件,case以前需關聯的接口
premise:

# 測試用例
test_case:
    - test_name: getSupportCity_1
      # 用例ID
      case_id: 1
      is_run:
      # 用例描述
      info: getSupportCity test
      # 請求參數
      parameter:
          byProvinceName: "四川"

      # 校驗列表
      check:
          check_type: Regular
          expected_result:
              - '成都 (56294)'
              - '廣元 (57206)'
          expected_code:

      global_relevance:
      # 關聯鍵
      relevance:

    - test_name: getSupportCity_2
      case_id: 2
      is_run:
      info: getSupportCity test
      parameter:
          byProvinceName: "四川"

      # 校驗列表
      check:
          check_type: no_check   #不校驗結果
          expected_code:
          expected_result:

      global_relevance:
      # 關聯鍵
      relevance:
  • 當該接口的參數數據較多時,爲維護方便,可將其保存在一個單獨的json文件中,好比上面用例中的data_parkinside.json,就是保存該接口參數數據的一個文件,與測試用例文件在同一個目錄下。測試執行時,經過解析該json文件中的test_name字段,獲取屬於自身用例的參數,參數文件的內容格式以下:

[
  {
    "test_name": "parkinside_1",
    "parameter": {
      "token": "asdgfhh32456asfgrsfss",
	  "vpl": "AJ3585"
    }
  },
  {
    "test_name": "parkinside_3",
    "parameter": {
      "vpl": "AJ3585"
    }
  }
]

該json文件保存了兩條用例的參數,經過用例名parkinside_1獲取到第一條用例的參數,經過用例名parkinside_3獲取到第三條用例的參數(json參數文件中的用例名需與yaml用例文件中的用例名一致)。

  • 當該接口的指望結果較長時,爲維護方便,可將其保存在一個單獨的json文件中,好比上面用例中的result_parkinside.json,就是保存該接口指望結果的一個文件,與測試用例文件在同一目錄下。測試執行時,經過解析該json文件中的test_name字段,獲取屬於自身用例的指望結果,指望結果文件的內容格式以下:

[
  {
      "json":
          {
            "vplInfo":
            {
              "userID":22,
              "username":"wuya",
              "vpl":"京AJ3585"
            },
            "Parking_time_long":"20小時18分鐘",
            "Parking fee":"20$"
          },
      "test_name": "parkinside_1"
  }
]

該json文件保存了一條用例的指望結果,經過用例parkinside_1獲取到第一條用例的指望結果(json文件中的用例名需與yaml用例文件中的用例名一致)。

  • 若該接口的測試用例須要引用函數或者變量,則可先在一個單獨的relevance.ini關聯配置文件中,定義好相關的變量和函數名,並進行拼接,後續可經過變量名,引入測試用例中,好比上面用例中的 ${sign}$ ,就是引用了關聯配置文件中的 sign 變量值,relevance.ini關聯配置文件的內容格式以下:

[relevance]
nonce=$RandomString(5)$
timestamp = $GetTime(time_type=now,layout=13timestamp,unit=0,0,0,0,0)$
sign = $SHA1(asdh, ${nonce}$, ${timestamp}$)$

上面配置中的nonce變量,引用了隨機函數RandomString,該隨機函數產生長度爲5的隨機數,這些函數的定義都已封裝在functions模塊中,在這裏只須要經過對應的函數名,並存入參數便可引用相關函數。變量timestamp引用了時間戳函數,在這裏將生成一個13位的時間戳,並傳給變量timestamp。變量sign則是引用了加密函數SHA1,這裏將會把字符串asdh、變量nonce的值和變量timestamp的值先拼接起來,而後再將拼接好的字符串傳給加密函數SHA1加密。而後便可在用例中引用變量sign,以下:


# 請求參數
  parameter:
        sign: ${sign}$    # 經過變量引用關聯值
        vpl: AJ3585

四、單接口用例執行腳本

單接口測試用例執行腳本,由程序根據yaml格式的測試用例文件自動生成,並根據相應yaml格式的測試用例文件所在的路徑生成當前用例執行腳本的保存路徑,且該用例執行腳本平時不須要人工維護,以下是接口parkinside的執行腳本test_parkinside.py的格式:


# -*- coding: utf-8 -*-

import allure
import pytest
import time
from Main import root_path, case_level, product_version, run_interval
from common.unit.initializeYamlFile import ini_yaml
from common.unit.initializePremise import ini_request
from common.unit.apiSendCheck import api_send_check
from common.unit.initializeRelevance import ini_relevance
from common.unit import setupTest

case_path = root_path + "/tests/TestCases/parkinsideApi"
relevance_path = root_path + "/common/configModel/relevance"
case_dict = ini_yaml(case_path, "parkinside")


@allure.feature(case_dict["test_info"]["title"])
class TestParkinside:

    @pytest.fixture(scope="class")
    def setupClass(self):
        """
        :rel: 獲取關聯文件獲得的字典
        :return:
        """
        self.rel = ini_relevance(case_path, 'relevance')     #獲取本用例初始公共關聯值
        self.relevance = ini_request(case_dict, case_path, self.rel)   #執行完前置條件後,獲得的本用例最新所有關聯值
        return self.relevance, self.rel


    @pytest.mark.skipif(case_dict["test_info"]["product_version"] in product_version,
                        reason="該用例所屬版本爲:{0},在本次排除版本{1}內".format(case_dict["test_info"]["product_version"], product_version))
    @pytest.mark.skipif(case_dict["test_info"]["case_level"] not in case_level,
                        reason="該用例的用例等級爲:{0},不在本次運行級別{1}內".format(case_dict["test_info"]["case_level"], case_level))
    @pytest.mark.run(order=case_dict["test_info"]["run_order"])
    @pytest.mark.parametrize("case_data", case_dict["test_case"], ids=[])
    @allure.severity(case_dict["test_info"]["case_level"])
    @pytest.mark.parkinside
    @allure.story("parkinside")
    @allure.issue("http://www.bugjira.com")  # bug地址
    @allure.testcase("http://www.testlink.com")  # 用例鏈接地址
    def test_parkinside(self, case_data, setupClass):
        """
        測試接口爲:parkinside
        :param case_data: 測試用例
        :return:
        """
        self.relevance = setupTest.setupTest(relevance_path, case_data, setupClass)

        # 發送測試請求
        api_send_check(case_data, case_dict, case_path, self.relevance)
        time.sleep(run_interval)


if __name__ == '__main__':

    import subprocess
    subprocess.call(['pytest', '-v'])

五、封裝請求協議apiMethod.py


def post(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
    """
    post請求
    :param header: 請求頭
    :param address: 請求地址
    :param request_parameter_type: 請求參數格式(form_data,raw)
    :param timeout: 超時時間
    :param data: 請求參數
    :param files: 文件路徑
    :return:
    """
    if 'form_data' in request_parameter_type:
        for i in files:
            value = files[i]
            if '/' in value:
                file_parm = i
                files[file_parm] = (os.path.basename(value), open(value, 'rb'))
        enc = MultipartEncoder(
            fields=files,
            boundary='--------------' + str(random.randint(1e28, 1e29 - 1))
        )
        header['Content-Type'] = enc.content_type

        response = requests.post(url=address, data=enc, headers=header, timeout=timeout, cookies=cookie)

    elif 'data' in request_parameter_type:
        response = requests.post(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)

    elif 'json' in request_parameter_type:
        response = requests.post(url=address, json=data, headers=header, timeout=timeout, files=files, cookies=cookie)

    try:
        if response.status_code != 200:
            return response.status_code, response.text
        else:
            return response.status_code, response.json()
    except json.decoder.JSONDecodeError:
        return response.status_code, ''
    except simplejson.errors.JSONDecodeError:
        return response.status_code, ''
    except Exception as e:
        logging.exception('ERROR')
        logging.error(e)
        raise


def get(header, address, data, timeout=8, cookie=None):
    """
    get請求
    :param header: 請求頭
    :param address: 請求地址
    :param data: 請求參數
    :param timeout: 超時時間
    :return:
    """
    response = requests.get(url=address, params=data, headers=header, timeout=timeout, cookies=cookie)
    if response.status_code == 301:
        response = requests.get(url=response.headers["location"])
    try:
        return response.status_code, response.json()
    except json.decoder.JSONDecodeError:
        return response.status_code, ''
    except simplejson.errors.JSONDecodeError:
        return response.status_code, ''
    except Exception as e:
        logging.exception('ERROR')
        logging.error(e)
        raise


def put(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
    """
    put請求
    :param header: 請求頭
    :param address: 請求地址
    :param request_parameter_type: 請求參數格式(form_data,raw)
    :param timeout: 超時時間
    :param data: 請求參數
    :param files: 文件路徑
    :return:
    """
    if request_parameter_type == 'raw':
        data = json.dumps(data)
    response = requests.put(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)
    try:
        return response.status_code, response.json()
    except json.decoder.JSONDecodeError:
        return response.status_code, ''
    except simplejson.errors.JSONDecodeError:
        return response.status_code, ''
    except Exception as e:
        logging.exception('ERROR')
        logging.error(e)
        raise


def delete(header, address, data, timeout=8, cookie=None):
    """
    delete請求
    :param header: 請求頭
    :param address: 請求地址
    :param data: 請求參數
    :param timeout: 超時時間
    :return:
    """
    response = requests.delete(url=address, params=data, headers=header, timeout=timeout, cookies=cookie)

    try:
        return response.status_code, response.json()
    except json.decoder.JSONDecodeError:
        return response.status_code, ''
    except simplejson.errors.JSONDecodeError:
        return response.status_code, ''
    except Exception as e:
        logging.exception('ERROR')
        logging.error(e)
        raise


def save_cookie(header, address, request_parameter_type, timeout=8, data=None, files=None, cookie=None):
    """
    保存cookie信息
    :param header: 請求頭
    :param address: 請求地址
    :param timeout: 超時時間
    :param data: 請求參數
    :param files: 文件路徑
    :return:
    """
    cookie_path = root_path + '/common/configModel/relevance/cookie.ini'
    if 'data' in request_parameter_type:
        response = requests.post(url=address, data=data, headers=header, timeout=timeout, files=files, cookies=cookie)

    elif 'json' in request_parameter_type:
        response = requests.post(url=address, json=data, headers=header, timeout=timeout, files=files, cookies=cookie)

    try:
        if response.status_code != 200:
            return response.status_code, response.text

        else:
            re_cookie = response.cookies.get_dict()
            cf = Config(cookie_path)
            cf.add_section_option('relevance', re_cookie)
            for i in re_cookie:
                values = re_cookie[i]
                logging.debug("cookies已保存,結果爲:{}".format(i+"="+values))
            return response.status_code, response.json()

    except json.decoder.JSONDecodeError:
        return response.status_code, ''
    except simplejson.errors.JSONDecodeError:
        return response.status_code, ''
    except Exception as e:
        logging.exception('ERROR')
        logging.error(e)
        raise
    ……………………

六、封裝方法apiSend.py:處理測試用例,拼接請求併發送


def send_request(data, project_dict, _path, relevance=None):
    """
    封裝請求
    :param data: 測試用例
    :param project_dict: 用例文件內容字典
    :param relevance: 關聯對象
    :param _path: case路徑
    :return:
    """
    logging.info("="*100)
    try:
        # 獲取用例基本信息
        get_header =project_dict["test_info"].get("headers")
        get_host = project_dict["test_info"].get("host")
        get_address = project_dict["test_info"].get("address")
        get_http_type = project_dict["test_info"].get("http_type")
        get_request_type = project_dict["test_info"].get("request_type")
        get_parameter_type = project_dict["test_info"].get("parameter_type")
        get_cookies = project_dict["test_info"].get("cookies")
        get_file = project_dict["test_info"].get("file")
        get_timeout = project_dict["test_info"].get("timeout")
    except Exception as e:
        logging.exception('獲取用例基本信息失敗,{}'.format(e))

    try:
        # 若是用例中寫了headers關鍵字,則用用例中的headers值(若該關鍵字沒有值,則會將其值置爲none),不然用全局headers
        get_header = data["headers"]
    except KeyError:
        pass
    try:
        # 替換成用例中相應關鍵字的值,若是用例中寫了host和address,則使用用例中的host和address,若沒有則使用全局傳入的默認值
        get_host = data["host"]
    except KeyError:
        pass
    try:
        get_address = data["address"]
    except KeyError:
        pass
    try:
        get_http_type = data["http_type"]
    except KeyError:
        pass
    try:
        get_request_type = data["request_type"]
    except KeyError:
        pass
    try:
        get_parameter_type = data["parameter_type"]
    except KeyError:
        pass
    try:
        get_cookies = data["cookies"]
    except KeyError:
        pass
    try:
        get_file = data["file"]
    except KeyError:
        pass
    try:
        get_timeout = data["timeout"]
    except KeyError:
        pass

    Cookie = None

    header = get_header
    if get_header:
        if isinstance(get_header, str):
            header = confManage.conf_manage(get_header, "header")  # 處理請求頭中的變量
            if header == get_header:
                pass

            else:
                var_list = re.findall('\$.*?\$', header)
                header = literal_eval(header)  # 將字典類型的字符串,轉成字典
                # 處理請求頭中的變量和函數
                if var_list:
                    # 將關聯對象裏的鍵值對遍歷出來,並替換掉字典值中的函數
                    rel = dict()
                    for key, value in header.items():
                        rel[key] = replace_random(value)
                    header = rel
                    logging.debug("替換請求頭中的函數處理結果爲:{}".format(header))

                    str_header = str(header)
                    var_list = re.findall('\${.*?}\$', str_header)
                    if var_list:
                        # 用自身關聯對象裏的變量值,替換掉自身關聯對象裏的變量
                        header = replaceRelevance.replace(header, header)

                        str_header = str(header)
                        var_list = re.findall('\$.*?\$', str_header)
                        if var_list:
                            # 再次將關聯對象裏的鍵值對遍歷出來,並替換掉字典值中的函數
                            rel = dict()
                            for key, value in header.items():
                                rel[key] = replace_random(value)
                            header = rel
                        else:
                            pass
                    else:
                        pass
                else:
                    pass
        else:
            pass
    else:
        pass
    logging.debug("請求頭處理結果爲:{}".format(header))

    if get_cookies is True:
        cookie_path = root_path + "/common/configModel/relevance"
        Cookie = ini_relevance(cookie_path, 'cookie')   # 爲字典類型的字符串
        logging.debug("cookie處理結果爲:{}".format(Cookie))
    else:
        pass

    parameter = readParameter.read_param(data["test_name"], data["parameter"], _path, relevance)    #處理請求參數(含參數爲文件的狀況)
    logging.debug("請求參數處理結果:{}".format(parameter))

    get_address = str(replaceRelevance.replace(get_address, relevance))  # 處理請求地址中的變量
    logging.debug("請求地址處理結果:{}".format(get_address))

    get_host = str(confManage.conf_manage(get_host, "host"))   # host處理,讀取配置文件中的host
    logging.debug("host處理結果:{}".format(get_host))
    if not get_host:
        raise Exception("接口請求地址爲空 {}".format(get_host))
    logging.info("請求接口:{}".format(data["test_name"]))
    logging.info("請求地址:{}".format((get_http_type + "://" + get_host + get_address)))
    logging.info("請求頭: {}".format(header))
    logging.info("請求參數: {}".format(parameter))

    # 經過get_request_type來判斷,若是get_request_type爲post_cookie;若是get_request_type爲get_cookie
    if get_request_type.lower() == 'post_cookie':
        with allure.step("保存cookie信息"):
            allure.attach("請求接口:", data["test_name"])
            allure.attach("用例描述:", data["info"])
            allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
            allure.attach("請求頭", str(header))
            allure.attach("請求參數", str(parameter))

        result = apiMethod.save_cookie(header=header, address=get_http_type + "://" + get_host + get_address,
                                  request_parameter_type=get_parameter_type,
                                  data=parameter,
                                  cookie=Cookie,
                                  timeout=get_timeout)

    elif get_request_type.lower() == 'post':
        logging.info("請求方法: POST")
        if get_file:
            with allure.step("POST上傳文件"):
                allure.attach("請求接口:",data["test_name"])
                allure.attach("用例描述:", data["info"])
                allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
                allure.attach("請求頭", str(header))
                allure.attach("請求參數", str(parameter))

            result = apiMethod.post(header=header,
                                    address=get_http_type + "://" + get_host + get_address,
                                    request_parameter_type=get_parameter_type,
                                    files=parameter,
                                    cookie=Cookie,
                                    timeout=get_timeout)
        else:
            with allure.step("POST請求接口"):
                allure.attach("請求接口:", data["test_name"])
                allure.attach("用例描述:", data["info"])
                allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
                allure.attach("請求頭", str(header))
                allure.attach("請求參數", str(parameter))
            result = apiMethod.post(header=header,
                                    address=get_http_type + "://" + get_host + get_address,
                                    request_parameter_type=get_parameter_type,
                                    data=parameter,
                                    cookie=Cookie,
                                    timeout=get_timeout)
    elif get_request_type.lower() == 'get':
        with allure.step("GET請求接口"):
            allure.attach("請求接口:", data["test_name"])
            allure.attach("用例描述:", data["info"])
            allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
            allure.attach("請求頭", str(header))
            allure.attach("請求參數", str(parameter))
            logging.info("請求方法: GET")
        result = apiMethod.get(header=header,
                               address=get_http_type + "://" + get_host + get_address,
                               data=parameter,
                               cookie=Cookie,
                               timeout=get_timeout)
    elif get_request_type.lower() == 'put':
        logging.info("請求方法: PUT")
        if get_file:
            with allure.step("PUT上傳文件"):
                allure.attach("請求接口:", data["test_name"])
                allure.attach("用例描述:", data["info"])
                allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
                allure.attach("請求頭", str(header))
                allure.attach("請求參數", str(parameter))
            result = apiMethod.put(header=header,
                                    address=get_http_type + "://" + get_host + get_address,
                                    request_parameter_type=get_parameter_type,
                                    files=parameter,
                                    cookie=Cookie,
                                    timeout=get_timeout)
        else:
            with allure.step("PUT請求接口"):
                allure.attach("請求接口:", data["test_name"])
                allure.attach("用例描述:", data["info"])
                allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
                allure.attach("請求頭", str(header))
                allure.attach("請求參數", str(parameter))
            result = apiMethod.put(header=header,
                                    address=get_http_type + "://" + get_host + get_address,
                                    request_parameter_type=get_parameter_type,
                                    data=parameter,
                                    cookie=Cookie,
                                    timeout=get_timeout)
    elif get_request_type.lower() == 'delete':
        with allure.step("DELETE請求接口"):
            allure.attach("請求接口:", data["test_name"])
            allure.attach("用例描述:", data["info"])
            allure.attach("請求地址", get_http_type + "://" + get_host + get_address)
            allure.attach("請求頭", str(header))
            allure.attach("請求參數", str(parameter))
        logging.info("請求方法: DELETE")
        result = apiMethod.delete(header=header,
                               address=get_http_type + "://" + get_host + get_address,
                               data=parameter,
                               cookie=Cookie,
                               timeout=get_timeout)
    …………………………
    else:
        result = {"code": False, "data": False}
        logging.info("沒有找到對應的請求方法!")
    logging.info("請求接口結果:\n {}".format(result))
    return result

七、測試結果斷言封裝checkResult.py


def check_json(src_data, dst_data):
    """
    校驗的json
    :param src_data: 檢驗內容
    :param dst_data: 接口返回的數據
    :return:
    """
    if isinstance(src_data, dict):
        for key in src_data:
            if key not in dst_data:
                raise Exception("JSON格式校驗,關鍵字%s不在返回結果%s中" % (key, dst_data))
            else:
                this_key = key
                if isinstance(src_data[this_key], dict) and isinstance(dst_data[this_key], dict):
                    check_json(src_data[this_key], dst_data[this_key])
                elif isinstance(type(src_data[this_key]), type(dst_data[this_key])):
                    raise Exception("JSON格式校驗,關鍵字 %s 與 %s 類型不符" % (src_data[this_key], dst_data[this_key]))
                else:
                    pass
    else:
        raise Exception("JSON校驗內容非dict格式")


def check_result(test_name, case, code, data, _path, relevance=None):
    """
    校驗測試結果
    :param test_name: 測試名稱
    :param case: 測試用例
    :param code: HTTP狀態
    :param data: 返回的接口json數據
    :param relevance: 關聯值對象
    :param _path: case路徑
    :return:
    """
    # 不校驗結果
    if case["check_type"] == 'no_check':
        with allure.step("不校驗結果"):
            pass

    # json格式校驗
    elif case["check_type"] == 'json':
        expected_result = case["expected_result"]
        if isinstance(case["expected_result"], str):
            expected_result = readExpectedResult.read_json(test_name, expected_result, _path, relevance)
        with allure.step("JSON格式校驗"):
            allure.attach("指望code", str(case["expected_code"]))
            allure.attach('指望data', str(expected_result))
            allure.attach("實際code", str(code))
            allure.attach('實際data', str(data))
        if int(code) == case["expected_code"]:
            if not data:
                data = "{}"
            check_json(expected_result, data)
        else:
            raise Exception("http狀態碼錯誤!\n {0} != {1}".format(code, case["expected_code"]))

    # 只校驗狀態碼
    elif case["check_type"] == 'only_check_status':
        with allure.step("校驗HTTP狀態"):
            allure.attach("指望code", str(case["expected_code"]))
            allure.attach("實際code", str(code))
            allure.attach('實際data', str(data))
        if int(code) == case["expected_code"]:
            pass
        else:
            raise Exception("http狀態碼錯誤!\n {0} != {1}".format(code, case["expected_code"]))

    # 徹底校驗
    elif case["check_type"] == 'entirely_check':
        expected_result = case["expected_result"]
        if isinstance(case["expected_result"], str):
            expected_result = readExpectedResult.read_json(test_name, expected_result, _path, relevance)
        with allure.step("徹底校驗結果"):
            allure.attach("指望code", str(case["expected_code"]))
            allure.attach('指望data', str(expected_result))
            allure.attach("實際code", str(code))
            allure.attach('實際data', str(data))
        if int(code) == case["expected_code"]:
            result = operator.eq(expected_result, data)
            if result:
                pass
            else:
                raise Exception("徹底校驗失敗! {0} ! = {1}".format(expected_result, data))
        else:
            raise Exception("http狀態碼錯誤!\n {0} != {1}".format(code, case["expected_code"]))

    # 正則校驗
    elif case["check_type"] == 'Regular_check':
        if int(code) == case["expected_code"]:
            try:
                result = ""
                if isinstance(case["expected_result"], list):
                    with allure.step("正則校驗"):
                        for i in case["expected_result"]:
                            result = re.findall(i.replace("\"","\'"), str(data))
                            allure.attach('正則校驗結果\n',str(result))
                        allure.attach('實際data', str(data))
                else:
                    result = re.findall(case["expected_result"].replace("\"", "\'"), str(data))
                    with allure.step("正則校驗"):
                        allure.attach("指望code", str(case["expected_code"]))
                        allure.attach('正則表達式', str(case["expected_result"]).replace("\'", "\""))
                        allure.attach("實際code", str(code))
                        allure.attach('實際data', str(data))
                        allure.attach(case["expected_result"].replace("\"", "\'") + '校驗完成結果',
                                      str(result).replace("\'", "\""))
                if not result:
                    raise Exception("正則未校驗到內容! {}".format(case["expected_result"]))
            except KeyError:
                raise Exception("正則校驗執行失敗! {}\n正則表達式爲空時".format(case["expected_result"]))
        else:
            raise Exception("http狀態碼錯誤!\n {0} != {1}".format(code, case["expected_code"]))

    # 數據庫校驗
    elif case["check_type"] == "datebase_check":
        pass

    else:
        raise Exception("無該校驗方式:{}".format(case["check_type"]))

八、共享模塊conftest.py(初始化測試環境,製造測試數據,並還原測試環境)


import allure
import pytest
from common.configModel import confRead
from Main import root_path
from common.unit.initializeYamlFile import ini_yaml
from common.unit.initializeRelevance import ini_relevance
from common.unit.apiSendCheck import api_send_check
from common.configModel.confRead import Config
import logging
import os

conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config", "apiConfig.ini")
case_path = root_path + "/tests/CommonApi/loginApi"
relevance_path = root_path + "/common/configModel/relevance"

@pytest.fixture(scope="session", autouse=True)
def setup_env():
    # 定義環境;定義報告中environment
    Host = confRead.Config(conf_path).read_apiConfig("host")
    allure.environment(測試環境="online", hostName=Host["host"], 執行人="XX", 測試項目="線上接口測試")


case_dict = ini_yaml(case_path, "login")

# 參數化 fixture
@pytest.fixture(scope="session", autouse=True, params=case_dict["test_case"])
def login(request):
    # setup
    """
    :param request: 上下文
    :param request.param: 測試用例
    :return:
    """
    # 清空關聯配置
    for i in ["GlobalRelevance.ini", "ModuleRelevance.ini"]:
        relevance_file = os.path.join(relevance_path, i)
        cf = Config(relevance_file)
        cf.add_conf("relevance")

    logging.info("執行全局用例依賴接口,初始化數據!")
    relevance = ini_relevance(relevance_path, "ModuleRelevance")
    if request.param["case_id"] == 1:
        relevance = ini_relevance(case_path, "relevance")

    logging.info("本用例最終的關聯數據爲:{}".format(relevance))
    # 發送測試請求
    api_send_check(request.param, case_dict, case_path, relevance)
    logging.info("初始化數據完成!")
    yield
    # teardown
	# 還原測試環境部分代碼
	……
	……
    logging.info("本輪測試已結束,正在還原測試環境!")

九、測試執行總入口Main.py(收集測試用例,批量執行並生成測試報告)


import os
import shutil
import subprocess
import pytest
import logging
from common.unit.initializeYamlFile import ini_yaml
from common.utils.logs import LogConfig
from common.script.writeCase import write_case
from common.script.writeCaseScript import write_caseScript
from common.utils.formatChange import formatChange
from common.utils.emailModel.runSendEmail import sendEailMock

root_path = os.path.split(os.path.realpath(__file__))[0]
xml_report_path = root_path + "\\report\\xml"
detail_report_path = root_path + "\\report\\detail_report"
summary_report_path = root_path + "\\report\\summary_report\\summary_report.html"
runConf_path = os.path.join(root_path, "config")

# 獲取運行配置信息
runConfig_dict = ini_yaml(runConf_path, "runConfig")

case_level = runConfig_dict["case_level"]
if not case_level:
    case_level = ["blocker", "critical", "normal", "minor", "trivial"]
else:
    pass

product_version = runConfig_dict["product_version"]
if not product_version:
    product_version = []
else:
    pass

isRun_switch = runConfig_dict["isRun_switch"]
run_interval = runConfig_dict["run_interval"]
writeCase_switch = runConfig_dict["writeCase_switch"]

ProjectAndFunction_path = runConfig_dict["ProjectAndFunction_path"]
if not ProjectAndFunction_path:
    ProjectAndFunction_path = ""
else:
    pass

scan_path = runConfig_dict["scan_path"]
if not scan_path:
    scan_path = ""
else:
    pass

runTest_switch = runConfig_dict["runTest_switch"]
reruns = str(runConfig_dict["reruns"])
reruns_delay = str(runConfig_dict["reruns_delay"])
log = runConfig_dict["log"]


def batch(CMD):
    output, errors = subprocess.Popen(CMD, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
    outs = output.decode("utf-8")
    return outs


if __name__ == "__main__":
    try:
        LogConfig(root_path, log)

        if writeCase_switch == 1:
            # 根據har_path裏的文件,自動生成用例文件yml和用例執行文件py,若已存在相關文件,則再也不建立
            write_case(root_path, ProjectAndFunction_path)

        elif writeCase_switch == 2:
            write_caseScript(root_path, scan_path)

        else:
            logging.info("="*20+"本次測試自動生成測試用例功能已關閉!"+"="*20+"\n")

        if runTest_switch == 1:
            # 清空目錄和文件
            email_target_dir = root_path + "/report/zip_report"  # 壓縮文件保存路徑
            shutil.rmtree(email_target_dir)
            if os.path.exists(summary_report_path):
                os.remove(summary_report_path)
            else:
                pass
            os.mkdir(email_target_dir)

            args = ["-k", runConfig_dict["Project"], "-m", runConfig_dict["markers"], "--maxfail=%s" % runConfig_dict["maxfail"],
                    "--durations=%s" % runConfig_dict["slowestNum"], "--reruns", reruns, "--reruns-delay", reruns_delay,
                    "--alluredir", xml_report_path, "--html=%s" % summary_report_path]

            test_result = pytest.main(args)   # 所有經過,返回0;有失敗或者錯誤,則返回1

            cmd = "allure generate %s -o %s --clean" % (xml_report_path, detail_report_path)
            reportResult = batch(cmd)
            logging.debug("生成html的報告結果爲:{}".format(reportResult))

            # 發送report到郵件
            emailFunction = runConfig_dict["emailSwitch"]
            if emailFunction == 1:

                if test_result == 0:
                    ReportResult = "測試經過!"
                else:
                    ReportResult = "測試不經過!"

                # 將字符中的反斜槓轉成正斜槓
                fileUrl_PATH = root_path.replace("\\", "/")
                logging.debug("基礎路徑的反斜槓轉成正斜槓爲:{}".format(fileUrl_PATH))

                fileUrl = "file:///{}/report/summary_report/summary_report.html".format(fileUrl_PATH)
                logging.info("html測試報告的url爲:{}".format(fileUrl))
                save_fn = r"{}\report\zip_report\summary_report.png".format(root_path)
                logging.debug("轉成圖片報告後保存的目標路徑爲:{}".format(save_fn))
                formatChange_obj = formatChange()
                formatChange_obj.html_to_image(fileUrl, save_fn)

                email_folder_dir = root_path + "/report/detail_report"  # 待壓縮文件夾
                logging.debug("待壓縮文件夾爲:{}".format(email_folder_dir))
                sendEailMock_obj = sendEailMock()
                sendEailMock_obj.send_email(email_folder_dir, email_target_dir, runConfig_dict, ReportResult, save_fn)

            else:
                logging.info("="*20+"本次測試的郵件功能已關閉!"+"="*20+"\n")

        else:
            logging.info("="*20+"本次運行測試開關已關閉!"+"="*20+"\n")
    except Exception as err:
        logging.error("本次測試有異常爲:{}".format(err))

十、結合Allure生成報告

  • 好的測試報告在整個測試框架是相當重要的部分,Allure是一個很好用的報告框架,不只報告美觀,並且方便CI集成。

  • Allure中對嚴重級別的定義:

    1. Blocker級別:中斷缺陷(客戶端程序無響應,沒法執行下一步操做)
    2. Critical級別:臨界缺陷(功能點缺失)
    3. Normal級別:普通缺陷(數值計算錯誤)
    4. Minor級別:次要缺陷(界面錯誤與UI需求不符)
    5. Trivial級別:輕微缺陷(必輸項無提示,或者提示不規範)
  • Allure報告總覽,如圖所示:















  • 發送到郵件中的測試報告



  • 測試執行項目演示

pytest、Allure與Jenkins集成

一、集成環境部署

一、Linux安裝docker容器

  • 安裝docker容器腳本

    curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
  • 啓動docker

    systemctl start docker
  • 經過修改daemon配置文件/etc/docker/daemon.json來使用阿里雲鏡像加速器

    sudo mkdir -p /etc/docker
      sudo tee /etc/docker/daemon.json <<-'EOF'
      {
        "registry-mirrors": ["https://XXXX.XXXX.aliyuncs.com"]
      }
      EOF
      sudo systemctl daemon-reload
      sudo systemctl restart docker
  • 查看阿里雲加速器是否配置成功

    vi  /etc/docker/daemon.json

二、安裝Jenkins

  • 在 Docker 中安裝並啓動 Jenkins 的樣例命令以下:

    docker run -d -u root \
        --name jenkins-blueocean \
        --restart=always \
        -p 8080:8080 \
        -p 50000:50000 \
        -p 50022:50022 \
        -v /home/jenkins/var:/var/jenkins_home \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v "$HOME":/home \
        jenkinsci/blueocean
      
      其中的 50000 是映射到 TCP port for JNLP agents 對應的端口,50022 是映射到 SSHD Port。在成功啓動 Jenkins 後,可在Jenkins啓動頁面 http://ip:8080/configureSecurity/ 上設置。
      這兩個端口其實不是必須的,只是爲了方便經過 SSH 使用 Jenkins 纔開啓它們。
  • 在此頁面打開 SSHD Port 後,運行如下命令便可驗證對應的端口值。

    curl -Lv http://ip:8080/login 2>&1 | grep 'X-SSH-Endpoint'
  • 把Jenkins容器裏的密碼粘貼上

    /var/jenkins_home/secrets/initialAdminPassword
  • 訪問 http://ip:8080 ,安裝默認推薦插件

  • 先到admin配置界面,再次修改admin的用戶密碼

三、allure與jenkins集成

  • jenkins安裝插件
    在管理Jenkins-插件管理-可選插件處,搜索allure ,而後安裝,以下
    插件名稱爲Allure Jenkins Plugin,以下圖所示:





  • jenkins安裝allure_commandline(若以前已安裝過allure插件,則跳過此步,按第三步進行)
    若是jenkins上有安裝maven的話,則此工具安裝就比較簡單了,打開jenkins的Global Tool Configuration,找到Allure Commandline,選擇安裝,以下所示:


若是沒有安裝maven,則須要去jenkins服務器上安裝此工具。

  • 點擊管理Jenkins,打開jenkins的Global Tool Configuration,找到Allure Commandline
    配置已安裝的jdk的JAVA_HOME,如圖


  • 配置Allure Commandline,如圖

  • 針對Linux上的遠程從節點配置:

  1. 配置遠程從節點

  2. 將agent.jar下載到該遠程節點Linux的某個目錄中,而後在agent.jar所在的目錄下,執行所生成的節點命令,便可啓動節點,將該節點鏈接到Jenkins。
  • 針對Windows的遠程從節點配置:

    1. 配置遠程從節點

    2. 在Windows上啓動該節點

      將agent.jar下載到該遠程節點windows的某個目錄中,而後在agent.jar所在的目錄下,執行裏面的命令,好比java -jar agent.jar -jnlpUrl http://192.168.201.9:8080/computer/win10_jun/slave-agent.jnlp -secret 1db00accef84f75b239febacc436e834b2164615a459f3b7f00f77a14ed51539 -workDir "E:\jenkins_work"
      便可以將該節點鏈接到Jenkins,以下




    3. 新建job,配置以下,好比保留7天之內的build,並規定最多隻保留10個build



      編寫構建腳本

      在命令後,換行,寫上 exit 0 (加上exit 0表示執行完成退出)
      添加allure report插件

      配置生成的xml路徑和生成html報告的路徑










  • 設置郵件通知

  1. 安裝插件Email Extension



  2. 進入jenkins的系統管理-系統設置,進行相關配置





  3. 修改Default Content的內容,具體內容以下:

    $PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:
    
    Check console output at ${BUILD_URL}allure/ to view the results.





  1. 再進入【系統管理-系統設置】拉到最下面,設置問題追蹤,在Allure Report下選擇增長:

    Key: allure.issues.tracker.pattern
    
    Value: http://tracker.company.com/%s
  • 對構建的job添加郵件發送
  1. job配置頁面,添加構建後步驟「Editable Email Notification」,如圖

  2. 如下可使用默認配置:







  3. 在Default Content中定義郵件正文,模板以下


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次構建日誌</title>
</head>
 
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
    <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
        <tr>
            <td>(本郵件由程序自動下發,請勿回覆!)</td>
        </tr>
        <tr>
            <td>
                <h2><font color="#FF0000">構建結果 - ${BUILD_STATUS}</font></h2>
            </td>
        </tr>
        <tr>
            <td><br />
                <b><font color="#0B610B">構建信息</font></b>
                <hr size="2" width="100%" align="center" />
            </td>
        </tr>
        <tr><a href="${PROJECT_URL}">${PROJECT_URL}</a>
            <td>
                <ul>
                    <li>項目名稱:${PROJECT_NAME}</li>
                    <li>GIT路徑:<a href="${GIT_URL}">${GIT_URL}</a></li>                    
                    <li>構建編號:第${BUILD_NUMBER}次構建</li>                    
                    <li>觸發緣由:${CAUSE}</li>
                    <li>系統的測試報告 :<a href="${PROJECT_URL}${BUILD_NUMBER}/allure">${PROJECT_URL}${BUILD_NUMBER}/allure</a></li><br />
                    <li>構建日誌:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                </ul>
            </td>
        </tr>
        <tr>
            <td>
                <b><font color="#0B610B">變動信息:</font></b>
               <hr size="2" width="100%" align="center" />
            </td>
        </tr>
        <tr>
            <td>
                <ul>
                    <li>上次構建成功後變化 :  ${CHANGES_SINCE_LAST_SUCCESS}</a></li>
                </ul>    
            </td>
        </tr>
 <tr>
            <td>
                <ul>
                    <li>上次構建不穩定後變化 :  ${CHANGES_SINCE_LAST_UNSTABLE}</a></li>
                </ul>    
            </td>
        </tr>
        <tr>
            <td>
                <ul>
                    <li>歷史變動記錄 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
                </ul>    
            </td>
        </tr>
        <tr>
            <td>
                <ul>
                    <li>變動集:${JELLY_SCRIPT,template="html"}</a></li>
                </ul>    
            </td>
        </tr>

        <hr size="2" width="100%" align="center" />
 
    </table>

</body>
</html>
  • 在Jenkins上啓動測試,如圖

  • 啓動測試產生的過程日誌以下

  • 測試構建完成結果以下

  • 構建完成後,Jenkins自動發送的郵件報告以下

  • CI集成後,產生的Allure報告以下

  • Jenkins中啓動測試項目演示

相關文章
相關標籤/搜索