淺談倉儲UI自動化之路

京東雲開發者發表於2023-11-16

1 分層測試

分層測試:就是不同的時間段,不同的團隊或團隊使用不同的測試用例對產品不同的關注點進行測試。一個系統/產品我們最先看到的是UI層,也就是外觀或者說整體,這些是最上層,最上層依賴下面的服務層,也就是介面或者模組,最底層就是單元,這個單元是函式或者方法。按照這三層選擇不同時間段,不同團隊不同測試用例進行的測試就是分層測試。

通讀上述概念,先對分層測試有個大體的印象,下面結合測試金字塔模型來具體說明:

1.1 單元(Unit )測試

單元測試是針對程式碼單元(通常是類/方法)的測試,單元測試的價值在於能提供最快的反饋,在開發過程中就可以對邏輯單元進行驗證。

1.2 介面(Service/服務/API)測試

介面測試是針對業務介面進行的測試,主要測試內部介面功能實現是否完整。如主要業務流是否能走通,異常處理是否正確,資料為空時校驗等等。介面測試的主要價值在於介面定義相對穩定,不像介面或底層程式碼會經常發生變化,所以介面測試比較容易編寫,用例的維護成本也相對較低。在介面層面準備測試的價效比相對較高。

1.3 整合(UI)測試

整合測試從使用者的角度驗證產品功能的正確性,測的是端到端的流程,並且加入使用者場景和資料,驗證整個業務流。整合測試的業務價值最高,它驗證的是一個完整的流程,但因為需要驗證完整流程,在環境部署、準備用例及實施等方面成本較高,實施起來並不容易。

1.4 分層測試總結

Google的自動化分層投入佔比是:單元測試(Unit):佔比70%;介面測試(Service):佔比20%;整合測試(UI):佔比10%。

測試過程中需要儘量提早介入測試,針對重點模組功能進行摸底測試,根據金字塔模型 越往上,越接近QA、業務和終端使用者,發現問題後解決問題的成本會越高。採用分層測試存在以下優勢:

  • 儘量測試前移,在開發前期發現問題解決問題,開發成本會迅速下降。
  • 不同時間段關注不同,分重點測試,層層防護。
  • 容易定位問題,測的哪一層,出現問題,就是哪一層的問題,很明確。
  • 分層測試在用例設計和執行測試的時候,更具有針對性,思維更加清晰,不容易遺漏。
  • 加強測試對程式碼實現的理解,可以更好的進行測試技能擴充。

最後,在具體實施時,層級如何劃分要設計好,設計好對應層級的測試用例,且用例執行時要持續追蹤,前面的工作要為後面的工作起到實際作用。

2 UI自動化

UI自動化測試,即透過模擬手動操作使用者UI介面的方式,以程式碼方式實現自動操作和驗證的一種自動化測試手段。從測試渠道上可以分為WebUI測試和App測試,WebUI包括PC和H5兩個方向。

2.1 UI自動化作用

  • 重複性的功能測試及驗證;
  • 避免疲憊操作時的人為測試遺漏;
  • 透過UI自動化操作獲取其他測試資料的能力。

2.2 UI自動化優點

  • 用例編寫簡單,降低上手門檻;
  • 節省人工測試成本,提高功能測試、迴歸測試的測試效率;
  • 保障軟體質量的一種手段和方式。

2.3 UI自動化缺點

  • UI控制元件的頻繁變更導致控制元件定位;
  • 用例指令碼的維護成本較高,投入和產出比例低;
  • 元素定位的不穩定導致用例的效率和穩定性差。

3 常見的UI自動化框架分析

常用的WebUI自動化測試工具主要有強大且免費開源的Selenium家族,也有體驗良好收費很貴的QTP工具,還有新興崛起的Cypress,以及其他工具。

3.1 Cypress和Selenium使用者量對比

Cypress和Selenium的下載量對比分析:Selenium相對穩定,Cypress下載量在21年度正式超過了Selenium,並且分差不斷拉大。

3.2 Cypress和Selenium實現架構對比

Selenium系統的架構:由程式碼透過JSON Wire網路協議和driver進行通訊,由driver和真實瀏覽器互動操作,最終返回操作結果到程式碼。

Cypress系統的架構:使用 webpack 將測試程式碼中的所有模組 bundle 到一個 js 檔案中。測試程式碼和被測程式在同一個瀏覽器的不同iframe 中,無需透過網路訪問。

3.3 Cypress和Selenium環境框架對比

Cypress和Selenium自動化環境搭建對比:Cypress只需要安裝的方式即可使用,Selenium作為庫包形式提供,需要自己選擇對應的框架,斷言以及額外的依賴。

3.4 Cypress和Selenium環境對比彙總

4 如何做好UI自動化

4.1 我不想寫UI自動化的N個理由

  • 自動化編寫指令碼成本高,selenium需要自己搭框架,cypress只能用js寫,且都需要對前端有所涉略才能寫好。
  • 自動化手動編寫指令碼案例需要很長時間,編寫場景數過少,發現不了多少問題。
  • 錄製、編寫時候頁面有變化時候每個用例都需要修改,需要專人維護,維護成本非常高。
  • 經常出現指令碼問題出現的錯誤,頁面載入異常等等,寫出來的的用例非常不穩定。

4.2 我不得不寫UI自動化的N個理由

  • 需要回歸測試的場景太多,手工重複頻繁執行太耗時。
  • 需要進行線上環境測試,無線上介面操作許可權。
  • UI自動化測試更貼近使用者實際使用場景,透過介面測試無法完全保障質量。

4.3 如何做好UI自動化—降低程式碼維護成本

針對不想寫UI自動化又不得不做的時候,我們需要選擇一個好的框架來管理我們的自動化用例。不管是selenium還是cypress,我們都需要將我們的自動化指令碼程式碼儘量的複用。如果我們只是想測試流程資料的時候,需要將我們控制元件和操作進行封裝。下面我們將主要用倉儲Cypress自動化來舉例子。

4.3.1 基礎控制元件進行封裝

一般系統的基礎封裝控制元件是有一定風格的,譬如下圖中我們如何快速尋找到訂單號的輸入框呢?顯然如果直接透過“請輸入”欄位查詢會查到多個,無法定位唯一。針對倉儲系統由於頁面都是配置出來的,沒有固定唯一的id管理。

透過dom樹分析得出,普通的輸入框可以透過label名稱查詢到對應的label,然後查詢到公共的父節點el-form-item,再根據查詢el-form-item的子節點el-form-item__content的子節點el-input來查詢到要輸入的輸入框進行輸入內容。

將此操作封裝為兩個基礎自定義命令:

//查詢label所在的el-form-item控制元件組合根節點,正則全詞匹配更加精確
Cypress.Commands.add('getElFormItemByLabel', (label) => {
  cy.get('.el-form-item__label').contains(new RegExp("^" + label + "$", "g")).first().parent('.el-form-item')
})


// 輸入框填寫值,根據傳入的el-form-item找到el-input-inner物件進行輸入,增加去除readonly事件,enter事件,以及強制輸入
Cypress.Commands.add("cTypeWidthEvent", { prevSubject: 'element' }, ($elSelect, value, event = 'enter') => {
  cy.wrap($elSelect).find('.el-input__inner').then(($el)=>{
    $el.removeAttr('readonly')
    }).type(value + `{${event}}`,{force:true})
})


//後續使用輸入框時候只需要這樣使用
cy.getElFormItemByLabel('訂單號').cTypeWidthEvent(orderNo)
cy.getElFormItemByLabel('派車單號').cTypeWidthEvent(TJNo)

根據dom樹分析可以得到el-button class樣式的span標籤內容是設定,找到此唯一節點點選即可實現點選事件。

//封裝點選自定義命令
Cypress.Commands.add('btnClick', (label) => {
    cy.get('button > span').contains(new RegExp("^" + label + "$", "g")).parent('button').click()
})

//如下使用封裝所有頁面的點選事件
cy.btnClick('設定')

透過如上的方法首先將系統的基礎操作元素都封裝到customCommands中,作為系統基礎控制元件管理。可以極大程度上降低維護控制元件變化的成本。

4.3.2 Page-Object模式針對系統頁面進行管理

將如下的訂單列表頁面進行頁面封裝,譬如我在出庫單據中心只會做根據訂單號搜尋,然後點選訂單號跳轉進入詳情頁。

只是需要建一個訂單PO管理的頁面class,由於倉儲是用來跑流程的所以只封裝一些用到的控制元件即可。

//建立PO管理,透過封裝的基礎命令來封裝控制元件操作,此最好做到只維護控制元件名稱資料
export default class OrderCenterListPage{
    constructor() {}
    clearReceiveOrderDate(receiveStartOrderDate,receiveEndOrderDate){
        //刪除接單時間
        cy.getElFormItemByLabel('接單時間').children('.el-form-item__content').find('.el-icon-clear').first().click({force:true})
    }

    orderNoFilterInput(orderNo){
        cy.getElFormItemByLabel('訂單號').cTypeWidthEvent(orderNo)
    }

    openOrderDetail(orderNo){
        cy.get('.el-table_1_column_3').contains(orderNo).click()
    }
}

//同時將此頁面的操作封裝成一個自定義頁面操作命令
// 按訂單查詢明細
Cypress.Commands.add('OrderCenterListPage.openOrderDetail', (orderNo) => {
    var page = new OrderCenterListPage();
    page.clearReceiveOrderDate()
    var routeName = 'queryOrderListInfo'+ Date.now()
    cy.intercept('POST','**/order/web/queryOrderListInfo').as(routeName)
    page.orderNoFilterInput(orderNo)
    cy.wait(`@${routeName}`).then((res=>{
        page.openOrderDetail(orderNo)
    }))
})

4.3.3 針對操作流程進行再度封裝

上述封裝如果針對頁面測試的話已經夠了,如果是要針對流程測試,類似倉儲的出庫流程中有如下的步驟,需要再次對每個頁面操作進行封裝到一個流程當中,進一步提升程式碼複用。

基於此,我們做了生產流程的封裝,見如下圖:

4.3.4 測試用例組織

首先新建一個測試套件,然後將每個頁面的主要操作封裝成流程的一個用例。為什麼要這樣做呢?將測試用例拆散驗證方便失敗重試,如果你寫的在一個it裡面意味著失敗重試需要全量觸發,反之只需要重試其中一個步驟即可,大大提升成功的效率和縮短重試執行的時間。還可以快速的發現問題所在的位置。

import ExceptionInBoundFlow from '../../support/flow/exceptionInBoundFlow'
import passBackList from '../../fixtures/0_990/passback.json'

describe('5.0到6.0切倉出庫全流程驗證', () => {

  const flow = new ExceptionInBoundFlow()
  const ibOrderNo = 'UAT_'+Date.now()
  const oBOrderNo = 'WMSESL140760105630761'   //WMSESL140760105632249

  before(()=>{
    cy.clearCookies()
  })

  after(()=>{
    //cy.clearCookies()
  })

  beforeEach(() => {
    //登陸系統
    cy.intercept('**/*',(req) => {
      req.headers['origin'] = 'http://sunlon.wms.jdl.cn'
    }).as('headers')

    cy.visit(Cypress.env('baseUrl'))

  })

  it('1.查詢生產流程和生產狀態',  () => {
    flow.getOrderProductionInfo(oBOrderNo)
  })

  it('2.根據定位異常下單',  () => {
    flow.receiveIbOrder({oBOrderNo:oBOrderNo,ibOrderNo:ibOrderNo})
  })

  it('3.掃描收貨',  () => {
    flow.scanReceiving({orderId:ibOrderNo,locationNo:Cypress.env('locationNo').pickLocation})
  })

  it('4.重新定位',  () => {
    flow.reLocate(oBOrderNo)
  })

  it('5.是否手工定位',  () => {
    flow.manualLocation(oBOrderNo)
  })

  it('6.任務分配',  () => {
    flow.createOutboundTask(oBOrderNo)
  })

  it('7.揀貨',  () => {
    flow.pickNew(oBOrderNo,Cypress.env('locationNo').pickLocation)
  })

  it('8.前合流',  () => {
    flow.confluenceBeforeCheck()
  })

  it('9.複核',  () => {
    flow.check({platformNo:Cypress.env('review').defaultPlatformNo,containerNo:Cypress.env('review').defaultContainerNo, palletNo:null, defaultConsumableCode:Cypress.env('review').defaultConsumableCode})
  })

  it('10.後合流上架',  () => {
    flow.upToShipmentLocation(oBOrderNo,Cypress.env('locationNo').fahuoLocation)
  })

  it('11.客單生成包裹',  () => {
    flow.createPackage(oBOrderNo)
  })

  it('12.發貨',  () => {
    flow.quickShip(oBOrderNo)
  })

  describe('13.校驗生產單回傳',  () => {
    for(const index in passBackList){
        const node = passBackList[index].node
        const whiteList = passBackList[index].whiteList
        const desc = passBackList[index].desc
        it(`校驗生產單回傳節點${index},訂單號=${oBOrderNo},回傳節點=${node},回傳名稱${desc},校驗內容校驗欄位=${whiteList}`,  () => {
          cy.log(`校驗生產單回傳節點,訂單號=${oBOrderNo},回傳節點=${node},遮蔽校驗欄位=${whiteList}`).then(()=>{
            flow.passBackCompare(oBOrderNo, passBackList[index].node,passBackList[index].whiteList)
          })
        })
    }
  })
})

以上為UI自動化(實際上不限於UI)降低程式碼指令碼維護成本的方法。

4.4 如何做好UI自動化—提升指令碼效率以及穩定性

4.4.1 去掉等待

我們在編寫指令碼時候由於一些操作需要等待後臺介面的返回才能進行下一步操作,我們可能會增加cy.wait(10000)設定等待時長10秒來處理。這種如果介面沒有10秒內返回的話,會導致用例的失敗。針對此我們採用了 cy.intercept來設定攔截介面路由,透過wait來等待後臺介面返回後再進行下一步操作。

   //設定路由名稱
    var routeName = 'queryOrderListInfo'+ Date.now()
    cy.intercept('POST','**/order/web/queryOrderListInfo').as(routeName)
 //操作頁面按鈕觸發請求
    page.orderNoFilterInput(orderNo)
    //等待頁面請求結束後進行下一步操作
    cy.wait(`@${routeName}`).then((res=>{
        page.openOrderDetail(orderNo)
    }))

用 cy.intercept除了可以解決操作問題外,還可以用來進行判斷斷言介面返回值,並且根據介面返回值進行重試操作,增強穩定性。

cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo1')
    //點選查詢
    page.search()
    cy.wait('@queryWaitTaskAssignOrderInfo1').then((res) => {
        // 針對響應進行斷言
        if(res.response.body.resultValue.total != 1){
            console.log('查詢待組單的訂單不成功,可能是未定位完成,再等待一分鐘')
            cy.wait(30000)
            cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo2')

            page.search()
            cy.wait('@queryWaitTaskAssignOrderInfo2').then((res) => {
                // 針對響應進行斷言
                if(res.response.body.resultValue.total != 1){
                    console.log('查詢待組單的訂單不成功,可能是未定位完成,再等待一分鐘')
                    cy.wait(60000)
                    cy.intercept('**/queryWaitTaskAssignOrderInfo').as('queryWaitTaskAssignOrderInfo3')
                    page.search()
                    cy.wait('@queryWaitTaskAssignOrderInfo3')
                }
            })
        }
    })

最後還可以透過 cy.intercept修改請求屬性,並且設定介面mock結果來解決外部介面依賴問題。

//設定所有請求新增請求origin
cy.intercept('**/*',(req) => {
req.headers['origin'] = 'http://sunlon.wms.jdl.cn'
}).as('headers')

//將widgets介面mock掉
cy.intercept('POST', 'http://example.com/widgets', {
  statusCode: 200,
  body: 'it worked!'
})

4.4.2 資料傳遞

Cypress自動化透過上面4.3.4方式設計用例,提升穩定性問題,同時也帶來了用例間如何傳遞資料的問題。在Selenium中同步傳遞資料使用一個全域性變數可以解決,在Cypress中由於每個操作都是非同步的,全域性變數方法不可行。這裡我們可以採用讀寫檔案方式,讀寫cookie方式,配置檔案方式讀取靜態變數。這裡我們介紹一下cookie的方法(需要注意:cookie中是不支援中文的,如果有中文會系統異常報錯)。

//配置cookie全域性生效:
Cypress.Cookies.defaults({
    preserve:/^testData*/
})
//獲取生產流程和生產狀態寫入cookie:
getOrderProductionInfo(oBOrderNo){
            var json = JSON.parse(res.resultValue[0].json)
            //獲取單據型別對映
            this.#testData.shipmentOrderType=json.ruleDetail[0].value[0] 
            //獲取生產流程
            this.#testData.productionInfo.location.mode=json.outboundProcessDto.locatingRuleVo.operationMode
            this.#testData.productionInfo.splitOrder.mode=json.outboundProcessDto.splitOrderRuleVo.operationMode
             //單據生產流程和狀態存入快取,注意快取不能放漢字
             cy.setCookie('testData.info.productInfo',JSON.stringify(this.#testData))
            })
 })
//cookie讀取使用:
    receiveIbOrder({oBOrderNo,ibOrderNo,wait=30000}){
        //獲取生產流程和上一步測試資料
        cy.getCookie('testData.info.productInfo').then(cookie=>{
             //儲存sku和是否需要採購收貨狀態
             cy.setCookie('testData.info.productInfo',JSON.stringify(testData))
         })
    })

透過動態資料獲取傳遞針對同一個訂單不但實現了不同生產流程的通用執行操作,還實現了基於狀態的重試,始得整個用例未成功的時候可以設定重新執行保證測試透過。

4.4.3 非同步Promise獲取方式

透過引入非同步Promise的方式將程式碼中原先需要then層層遞進的方法進行非同步平鋪返回結果。(需要注意的是不能在測試用例中將it改成async非同步屬性)

 export async function doTaskAsign({orderNo,orderType,pickType}){
    var batchNo = await promisify(cy['assembleFormCreat.doTaskAssign']())
    return batchNo
}

4.4.4 遮蔽系統異常提升穩定性

透過在support中引入如下配置解決系統錯誤報錯,非cypress斷言報錯引起的失敗現象。

Cypress.on('uncaught:exception', (err, runnable) => {
    // returning false here prevents Cypress from
    // failing the test
    return false
})

4.4.5 使用錄製來輔助定位複雜的控制元件

作為初級使用者,可能透過錄制能更好更快的學習語法,Cypress也支援錄製的方式。
只需要配置: “experimentalStudio”: true

4.4.6 漂亮的工程結構框架

5 學習UI自動化的途徑

Selenium本文沒做介紹,比較成熟的框架網上也有一堆專案案例,各個公司也有類似開發的錄製工具。Cypress可以學習小菠蘿測試筆記101篇總結,該位大佬基本上介紹了Cypress所有的API使用實踐,測試組這邊也有一本實體書可以供參考學習,遇到問題還可以去Cypress社群群求助。

其他就不多說了,歡迎大家一起來學習Cypress自動化,為我們的自動化事業添磚加瓦

作者:京東物流 徐桂貴

來源:京東雲開發者社群 自猿其說 Tech 轉載請註明來源

相關文章