一個基於多介面的業務自動化測試框架

陆压發表於2023-12-19

這是一個成熟的框架,不是要讓別人當小白鼠,它已經先後在兩家公司的 5 條業務線進行了推廣應用,用例條數到了幾千條以上,並且從 18 年開始每天都在 CI/CD 流程中被呼叫執行。

已有那麼多介面測試框架,為什麼重複造輪子?首先,本框架如題目描述,適用於多介面的業務自動化測試,不是簡單的介面測試框架;其次框架始於 17、18 年,當時也沒有現在如此多的介面測試框架。

程式碼地址

框架介紹

介面自動化測試無疑是測試提效最為行之有效的方案,市面上的介面自動化測試框架很眾多,而本框架與其它框架的區別如以下:

  • 用例程式碼編寫簡單,讓使用者精力集中在所測試系統的業務邏輯上,而 http 介面的定義,請求的傳送,測試報告資訊等都由框架完成
  • 不只適用於單個介面的測試,同樣適用於多個介面組成的完整的業務邏輯的測試,這往往是介面自動化測試更應該做到的
  • 登入等前置的業務操作也由框架完成,用例中只需引用相應 cookie
  • 框架同樣支援環境、各類賬號以及其它測試物料資訊維護
  • 簡單易用,java 小白也能在半小時內學會使用

框架結構

上手指南

工程結構說明

下面是一個論壇登入、瀏覽帖子、帖子點贊這樣一個簡單的業務場景進行舉例,如何用框架完成這一幾步操作的

定義 http 介面

介面定義是在 yml 檔案中,建議按照被測系統維護 yml 檔案

api:
  globalVariables:
    - UA: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"
  pioneers:
    - name: testerhome登入
      id: testerhomeLogin
      priority: 1
      path: https://$testerhomeHost/account/sign_in
      method: post
      headers: >-
        \{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","User-Agent":"$UA","x-requested-with":"XMLHttpRequest",
        "cookie":"user_id=eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1UVTJOamM9IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--43f5d4f117b5459e67c85cc6c569820abb1e6068; _homeland_session=Y2ljEAtdhRcbEHaTSSHMb3%2FUyn0aLrFrHoEP8QVjVq%2BvXMCEi9n57WDgHBw40L%2Bo%2Fghe148%2B%2B429DbYDWNAiC4FBFYFnEghtzkQWPpKsOm21DZQkUDLvYqr4Z2ylpkiGHqjpppkhw0LLke61psEh7ZKQte3Ia3TTzTSu9ifDtHEl9FBlZUXNgwi%2F5kscioZqkobTyJpCGp5M4mSrLiunIZUHbgm05AuWa5%2Bu2TwgsxOfpdAumg6Q0SoT7ipMLaGaprobuP0Kj2q5ZH4CKqG7fb%2FU0WwzsTgTCtMXaWLz5WYHizGKRD5CWysSMseGn5I%3D--5LouY27EpiVkGarr--tpTXhgdFShw4Qyn6sThkpg%3D%3D",
        "x-csrf-token":"zr6fgSyPS5nyqcwGdzD7R6T51aAK6L9Dv42Lao0CSPZo4jEn3pT5fNN2eTk84VdmqhzQasF+sdHQrvvxsLYSmg=="\}
      parameters: user[login]=&user[password]=&user[remember_me]=0&user[remember_me]=1&commit=登入
      extractors: \[{"name":"token","value":"cookies"}\]
  requests:
    - name: 讀帖子
      id:  topics
      path: https://$testerhomeHost/topics/38484
      method: get
      headers: >-
        \{"User-Agent":"$UA","Content-Type":"application/x-www-form-urlencoded","cookie":"$token","x-requested-with":"XMLHttpRequest","x-csrf-token":"r3E8899sEAEnqST2dmtIEluqG5C/nL/Rwp2l4ITtNDU3XpF4eULhClMRoWweMt6XWSmBn2H8fmPRas+CVkA/BA=="\}
    - name: 點贊
      id:  likes
      path: https://$testerhomeHost/likes
      method: post
      headers: >-
        \{"User-Agent":"$UA","Content-Type":"application/x-www-form-urlencoded","cookie":"$token","x-requested-with":"XMLHttpRequest","x-csrf-token":"r3E8899sEAEnqST2dmtIEluqG5C/nL/Rwp2l4ITtNDU3XpF4eULhClMRoWweMt6XWSmBn2H8fmPRas+CVkA/BA=="\}
      parameters: type=Topic&id=38484

如上,介面定義檔案大體分為三部分:globalVariables,pioneers,requests。

  • globalVariables:定義全域性變數,為 key、value 形式
  • pioneers 定義前置介面,用於定義登入等前置介面。程式啟動後、用例開始執行前,會自動先執行 pioneers 中定義的介面。 其中 name 隨意起;id 要唯一,建議按照介面請求地址的縮寫命名 id 屬性;priority,整數型別,當 pioneers 中定義了多個介面,執行時會按照 priority 屬性排序,之後順序執行。extractors:介面返回內容的提取,name,為提取的變數命名,後面介面可以透過 $name 名對其進行引用;value,變數的提取內容,支援提取 cookie 或返回 json 字串中的某個屬性 (填寫屬性的 json path)
  • requests 定義介面,基本同 pioneers 部分,少了 extractors 部分。

說明:此處的介面請求引數可以透過抓包工具抓包獲取,然後複製到這裡。介面定義只需定義一次,在用例中隨意獲取,使用介面時,根據需要設定請求引數,未設定的請求引數按照此處定義的值作為預設值。

用例程式碼:

@Test(enabled = true, description = "開啟帖子詳情頁→點贊")
public void test() {
    log.info("test start");
    //請求例項1,開啟帖子詳情頁
    Request request = Request.getInstance("topics");
    //請求1傳送
    Response response = request.doRequest();
    //返回為html,取其中的x_csrf_token,後面點贊介面用
    String html = response.asString();
    Headers  headers = response.getHeaders();
    Map<String, String> cookies = response.getCookies();
    Document document = Jsoup.parse(html);
    Element metaElement = document.select("meta[name=csrf-token]").first();
    String x_csrf_token = null;
    if (metaElement != null) {
        x_csrf_token = metaElement.attr("content");
    }
    //請求例項2,點贊介面
    request = Request.getInstance("likes");
    //更新cookie
    request.addCookies(cookies);
    if (x_csrf_token != null) {
        request.addHeader("x-csrf-token",x_csrf_token);
    }
    //傳送點贊請求
    response = request.doRequest();
    assertThat(response.getStatusCode()).isGreaterThanOrEqualTo(200).as("返回狀態碼校驗");
}

測試報告

如下圖,用例相關介面的請求資訊、返回資訊也都由框架自動記錄在了報告中,如有其它需要內容輸出到測試報告,可以在用例中新增 Report.log("要新增內容");

其它

  • 配置:如其它 spring 工程,配置檔案在 resources 目錄下,類似 pre、test 區分不同環境,application.properties 中定義一般的配置資訊(和環境無光),其中 pring.profiles.active=pre 來切換不同環境

  • 測試範圍定義:測試用例由 testng 維護,如框架中所示,詳細使用方法參見 testng 官網

    <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
    <suite verbose="1" name="bulls-test" >
    <listeners>
        <listener class-name="com.bulls.qa.service.CustomListener"></listener>
        <listener class-name="com.bulls.qa.service.NoticeListener"></listener>
    </listeners>
    <test name="bulls自動化" preserve-order="true">
        <parameter name="reruntimes" value="0"></parameter>
        <packages>
        </packages>
        <classes>
            <class name="com.bulls.qa.testcase.testerhome.Demo">
                <methods>
                    <include name="test"></include>
                </methods>
            </class>
        </classes>
    </test>
    </suite>
    
  • 執行:專案入口 com.bulls.qa.BullsApplication.main

//打包
mvn clean -DskipTests=true  package
//執行
java -jar target/bulls-0.6-SNAPSHOT.jar  測試範圍配置檔案.xml  

如上面例子,測試範圍配置檔案可以配置多個,執行時指定測試範圍,如不指定預設使用打包的程式程式碼中的測試範圍配置檔案

  • 測試報告:測試報導為單 html 檔案,方便 jenkins 配置展示,報告地址執行時所在目錄下 bulls.html
  • 斷言,選用的斷言框架為 AssertJ,AssertJ 的強大無需贅述,詳細使用方法參見 AssertJ 官網
assertThat(response.jsonPath().getList("recommendations")).size().isGreaterThan(0).as("recommendations長度大於0");
assertThat(response.jsonPath().getBoolean("has_more")).isTrue().as("has_more為true");
assertThat(response.jsonPath().getList("recommendations")).as("recommendations長度大於0").size().isEqualTo(3);
List<String> types = JsonPath.from(response.asString()).getList("recommendations.item_type");
String[] strs = "product,product-ad-card,deal,ad,shopping-curated-collection,auto-generated-collection,video,campaign-banner,benefit,web-view".split(",");
assertThat(strs).containsAll(types).as("types在列舉範圍內");
  • 傳送測試結果訊息通知,參見程式碼 NoticeListener,具體根據需要自行擴充套件
  • 介面傳參設定,較複雜的介面引數設定

相關介面定義

- name: 編輯商品
  id: itemEdit
  path: http://$mnghost/item/edit
  method: post
  cookies: $XXXXXXCookies
  headers: >-
    \{"User-Agent":"$UA","Content-Type": "application/json"\}
  parameters: >-
    \{"itemId":"2904"\}
- name: 新增商品
  id: itemSave
  path: http://$mnghost/item/save
  method: post
  cookies: $XXXCookies
  headers: >-
    \{"User-Agent":"$UA","Content-Type": "application/json"\}
  parameters: >-
    \{"itemId":"2913","categoryIdList":[1],"topCategoryName":"美食","itemName":"autoTest goods","limitNumber":3,
    "priceText":"","countDownCycle":"3","countDownLimit":"1","itemNo":"12sqw","delivery":"MANUAL",
    "image":"//yun.XXXXXX.com/images/202005/4su03vvahd.jpg","detail":"","itemStatus":"ON","skuProperties":[],
    "skuList":[{"id":3375,"stock":999999,"stockId":null,"sellingPrice":100,"originalPrice":100,"costPrice":100,
    "realPayPrice":100,"properties":null,"skuNo":"1","skuEnable":true,"changeStock":0}],"supportCOD":true,
    "originItemId":null,"merchantId":73,"tagIds":[],"id":2913,"topCategoryId":1,"itemShortName":"autoTest goo","url":null,
    "minPrice":100,"stock":0,"isRecommend":false,"minSkuOriginalPrice":null,"minSkuPriceDiff":null,"maxPriceDiff":null,
    "maxPriceDiffPrice":null,"maxPriceDiffOriginalPrice":null,"gmtModified":"2020-06-19 16:57:36","gmtModifyName":"測試專用",
    "gmtModifyEmail":"test@XXXXXX.com.cn","mainRecomIds":null,"merchantName":"autoTestShop01","merchantDelivery":"MANUAL",
    "imgHeight":[{"imgUrl":"http://yun.XXXXXX.com/images/202006/mj3yg07pj8.jpg","height":136},
    {"imgUrl":"http://yun.XXXXXX.com/images/202006/d47ad68hhc.jpg","height":372}],"mainImgUrl":null,"itemIntroduce":null,
    "saleLableUrl":null,"ssoDesc":null\}

相關程式碼

goodsId = 2904;
//編輯介面,獲取測試的商品資訊
Request request = Request.getInstance("itemEdit");
//直接設定,key-value形式
Response response = request.setParameter("itemId", goodsId).doRequest();
//庫存小於50,更新庫存
JsonPath jsonPath = response.jsonPath();
if (jsonPath.getBoolean("success") && jsonPath.getInt("data.stock") >= 50) {
    // dosomething
}
Map<String, Object> map = response.jsonPath().getMap("data");
if (map == null) {
    map = new HashMap<>();
}
map.put("itemId", goodsId);
map.put("stock", 9999999);
request = Request.getInstance("itemSave");
//遍歷介面的傳參結構定義,替換掉key完全匹配的那個map部分
request.setParameters(map);
//按照json path定位要設定的key
request.setParameter("$.skuList[0].stock", 9999999);
request.setParameter("$.skuList[0].changeStock", null);
//根據路徑刪除,路徑按json path
request.removeParameterByPath("$.skuList[0].stockId");
request.removeParameterByPath("$.skuList[0].id");
request.doRequest();
  • 詳盡的 json-path 使用方法,參見 JsonPath 使用

相關文章