打造心目中理想的自動化測試框架 (AppiumBooster)

debugtalk發表於2019-02-28

前言

做過自動化測試的人應該都會有這樣一種體會,要寫個自動化demo測試用例很容易,但是要真正將自動化測試落地,對成百上千的自動化測試用例實現較好的可複用性和可維護性就很難了。

基於這一痛點,我開發了AppiumBooster框架。顧名思義,AppiumBooster基於Appium實現,但更簡單和易於使用;測試人員不用接觸任何程式碼,就可以直接採用簡潔優雅的方式來編寫和維護自動化測試用例。

原型開發完畢後,我將其應用在當前所在團隊的專案上,並在使用的過程中,按照自己心目中理想的自動化測試框架的模樣對其進行迭代優化,最終打磨成了一個自己還算用得順手的自動化測試框架。

本文便是對AppiumBooster的核心特性及其設計思想進行介紹。在內容組織上,本文的各個部分相對獨立,大家可直接選擇自己感興趣的部分進行閱讀。

UI互動基礎

UI互動是自動化測試的基礎,主要分為三部分內容:定位控制元件、操作控制元件、檢測結果。

控制元件定位

定位控制元件時,統一採用元素ID進行定位。這裡的ID包括accessibility_idaccessibility_label,需要在iOS工程專案中預先進行設定。

另外,考慮到控制元件可能出現延遲載入的情況,定位控制元件時統一執行wait操作;定位成功後會立即返回控制元件物件,定位失敗時會進行等待並不斷嘗試定位,直到超時(30秒)後丟擲異常。

wait { id control_id }複製程式碼

原始碼路徑:AppiumBooster/lib/pages/control.rb

控制元件操作

根據實踐證明,UI的控制元件操作基本主要就是點選、輸入和滑動,這三個操作基本上可以覆蓋絕大多數場景。

  • scrollToDisplay: 根據指定控制元件的座標位置,對螢幕進行上/下/左/右滑動操作,直至將指定控制元件展示在螢幕中
  • click: 通過控制元件ID定位到指定控制元件,並對指定控制元件進行click操作;若指定控制元件不在當前螢幕中,則先執行scrollToDisplay,再執行click操作
  • type(text): 在指定控制元件中輸入字串;若指定控制元件不在當前螢幕中,則先執行scrollToDisplay,再執行輸入操作
  • tapByCoordinate: 先執行scrollToDisplay,確保指定控制元件在當前螢幕中;獲取指定控制元件的座標值,然後對座標進行tap操作
  • scroll(direction): 對螢幕進行指定方向的滑動

原始碼路徑:AppiumBooster/lib/pages/actions.rb

預期結果檢查

每次執行一步操作後,需要對執行結果進行判斷,以此來確定測試用例的各個步驟是否執行成功。

當前,AppiumBooster採用控制元件的ID作為檢查物件,並統一封裝到check_elements(control_ids)方法中。

在實際使用過程中,需要先確定當前步驟執行完成後的跳轉頁面的特徵控制元件,即當前步驟執行前不存在該控制元件,但執行成功後的頁面中具有該控制元件。然後在操作步驟描述的expectation屬性中指定特徵控制元件的ID。

具體地,在指定控制元件ID的時候還可以配合使用操作符(!,||,&&),以此實現多種複雜場景的檢測。典型的預期結果描述形式如下:

  • A: 預期控制元件A存在;
  • !A: 預期控制元件A不存在;
  • A||B: 預期控制元件A或控制元件B至少存在一個;
  • A&&B: 預期控制元件A和控制元件B同時存在;
  • A&&!B: 預期控制元件A存在,但控制元件B不存在;
  • !A&&!B: 預期控制元件A和控制元件B都不存在。

原始碼路徑:AppiumBooster/lib/pages/inner_screen.rb

測試用例引擎(YAML)

對於自動化測試而言,自動化測試用例的組織與管理是最為重要的部分,直接關係到自動化測試用例的可複用性和可維護性。

經過綜合考慮,AppiumBooster從三個層面來描述測試用例,從低到高分別是stepfeaturetestcase;描述方式推薦使用YAML格式。

steps(測試步驟描述)

首先是對於單一操作步驟的描述。

從UI層面來看,每一個操作步驟都可以歸納為三個方面:定位控制元件、操作控制元件和檢查結果。

AppiumBooster的做法是,將App根據功能模組進行拆分,每一個模組單獨建立一個YAML檔案,並儲存在steps目錄下。然後,在每個模組中以控制元件為單位,分別進行定義。

現以如下示例進行詳細說明。

---
AccountSteps:
  enter Login page:
    control_id: tablecellMyAccountLogin
    control_action: click
    expectation: btnForgetPassword

  input test EmailAddress:
    control_id: txtfieldEmailAddress
    control_action: type
    data: leo.lee@debugtalk.com
    expectation: sectxtfieldPassword

  check if coupon popup window exists(optional):
    control_id: inner_screen
    control_action: has_control
    data: btnViewMyCoupons
    expectation: btnClose
    optional: true複製程式碼

其中,AccountSteps是steps模組名稱,用於區分不同的steps模組,方便在features模組中進行引用。

描述單個步驟時,有三項是必不可少的:步驟名稱、控制元件ID(control_id)和控制元件操作方式(control_action)。當控制元件操作方式為輸入(type)時,則還需指定data屬性,即輸入內容。

在檢查步驟執行結果方面,可通過在expectation屬性中指定控制元件ID進行實現,前面在預期結果檢查一節中已經詳細介紹了使用方法。該屬性可以置空或不進行填寫,相當於不對當前步驟進行檢測。

另外還有一個optional屬性,對步驟指定該屬性並設定為true時,當前步驟的執行結果不影響整個測試用例。

features(功能點描述)

各個模組的單一操作步驟定義完畢後,雖然可以直接將多個步驟進行組合形成對測試場景的描述,即測試用例,但是操作起來會過於侷限細節;特別是當測試用例較多時,可維護性是一個很大的問題。

AppiumBooster的做法是,將App根據功能模組進行拆分,每一個模組單獨建立一個YAML檔案,並儲存在features目錄下。然後,在每個模組中以功能點為單位,通過對steps模組中定義好的操作步驟進行引用並組合,即可實現對功能點的描述。

系統登入功能為例,功能點的描述可採用如下形式。

---
AccountFeatures:
  login with valid test account:
    - AccountSteps | enter My Account page
    - AccountSteps | enter Login page
    - AccountSteps | input test EmailAddress
    - AccountSteps | input test Password
    - AccountSteps | login
    - AccountSteps | close coupon popup window(optional)

  login with valid production account:
    - AccountSteps | enter My Account page
    - AccountSteps | enter Login page
    - AccountSteps | input production EmailAddress
    - AccountSteps | input production Password
    - AccountSteps | login
    - AccountSteps | close coupon popup window(optional)

  logout:
    - AccountSteps | enter My Account page
    - SettingsSteps | enter Settings page
    - AccountSteps | logout複製程式碼

其中,AccountFeatures是features模組名稱,用於區分不同的features模組,方便在testcase中進行引用。

在引用steps模組的操作步驟時,需要同時指定steps模組名稱和操作步驟的名稱,並以|進行分隔。

testcases(測試用例描述)

在功能點描述的基礎上,AppiumBooster就可以在第三個層面,簡單清晰地描述測試用例了。

具體做法很簡單,針對每個測試用例建立一個YAML檔案,並儲存在testcases目錄下。然後,通過對features模組中定義好的功能點描述進行引用並組合,即可實現對測試用例的描述。

同樣的,在引用features模組的功能點時,也需要同時指定features模組名稱和功能點的名稱,並以|進行分隔。

如下示例便是實現了在商城中購買商品的整個流程,包括切換國家、登入、選擇商品、新增購物車、下單完成支付等功能點。

---
Buy Phantom 4:
  - SettingsFeatures | initialize first startup
  - SettingsFeatures | Change Country to China
  - AccountFeatures | login with valid account
  - AccountFeatures | Change Shipping Address to China
  - StoreFeatures | add phantom 4 to cart
  - StoreFeatures | finish order
  - AccountFeatures | logout複製程式碼

另外,在某些測試場景中可能需要重複進行某一個功能點的操作。雖然可以將需要重複的步驟多寫幾次,但會顯得比較累贅,特別是重複次數較多時更是麻煩。

AppiumBooster的做法是,在測試用例的步驟中可指定執行次數,並以|進行分隔,如下例所示。

---
Send random text messages:
  - SettingsFeatures | initialize first startup
  - AccountFeatures | login with valid test account
  - MessageFeatures | enter follower user message page
  - MessageFeatures | send random text message | 100複製程式碼

測試用例引擎(CSV)

基本上,YAML測試用例引擎已經可以很好地滿足組織和管理自動化測試用例的需求。

但考慮到部分使用者會偏向於使用表格的形式,因為表格看上去更直觀一些,AppiumBooster同時還支援CSV格式的測試用例引擎。

testcases(測試用例描述)

採用表格來編寫測試用例時,只需要在任意表格工具,包括Microsoft Excel、iWork Numbers、WPS等,按照如下形式對測試用例進行描述。

AppiumBooster CSV Testcase example

然後,將表格內容另存為CSV格式的檔案,並放置於testcases目錄中即可。

可以看出,CSV格式的測試用例和YAML格式的測試用例是等價的,兩者包含的資訊內容完全相同。

在具體實現上,AppiumBooster在執行測試用例之前,也會將兩個測試用例引擎的測試用例描述轉換為相同的資料結構,然後再進行統一的操作。

統一轉換後的資料結構如下所示:

{
  "testcase_name": "Login and Logout",
  "features_suite": [
    {
      "feature_name": "login with valid account",
      "feature_steps": [
        {"control_id": "btnMenuMyAccount", "control_action": "click", "expectation": "tablecellMyAccountSystemSettings", "step_desc": "enter My Account page"},
        {"control_id": "tablecellMyAccountLogin", "control_action": "click", "expectation": "btnForgetPassword", "step_desc": "enter Login page"},
        {"control_id": "txtfieldEmailAddress", "control_action": "type", "data": "leo.lee@debugtalk.com", "expectation": "sectxtfieldPassword", "step_desc": "input EmailAddress"},
        {"control_id": "sectxtfieldPassword", "control_action": "type", "data": 12345678, "expectation": "btnLogin", "step_desc": "input Password"},
        {"control_id": "btnLogin", "control_action": "click", "expectation": "tablecellMyMessage", "step_desc": "login"},
        {"control_id": "btnClose", "control_action": "click", "expectation": nil, "optional": true, "step_desc": "close coupon popup window(optional)"}
      ]
    },
    {
      "feature_name": "logout",
      "feature_steps": [
        {"control_id": "btnMenuMyAccount", "control_action": "click", "expectation": "tablecellMyAccountSystemSettings", "step_desc": "enter My Account page"},
        {"control_id": "tablecellMyAccountSystemSettings", "control_action": "click", "expectation": "txtCountryDistrict", "step_desc": "enter Settings page"},
        {"control_id": "btnLogout", "control_action": "click", "expectation": "uiviewMyAccount", "step_desc": "logout"}
      ]
    }
  ]
}複製程式碼

測試用例轉換器(yaml2csv

既然CSV格式的測試用例和YAML格式的測試用例是等價的,那麼兩者之間的轉換也就容易實現了。

當前,AppiumBooster支援將YAML格式的測試用例轉換為CSV格式的測試用例,只需要執行一條命令即可。

$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.yml複製程式碼

過程記錄及結果儲存

在自動化測試執行過程中,應儘量對測試用例執行過程進行記錄,方便後續對問題根據定位和追溯。

過程記錄方式

當前,AppiumBooster已實現的記錄形式有如下三種:

  • logger模組:可指定日誌級別對測試過程進行記錄
  • 截圖功能:測試用例執行過程中,在每個步驟執行完成後進行截圖
  • DOM source:測試用例執行過程中,在每個步驟執行完成後儲存當前頁面的DOM內容

測試結果儲存

由於Appium分為Server端和Client端,因此AppiumBooster在記錄日誌的時候也將日誌分為了三份:

  • appium_server.log: Appium Server端的日誌,這部分日誌是由Appium框架列印的
  • appium_booster.log: 包括測試環境初始化和測試用例執行記錄,這部分日誌是由AppiumBooster中採用logger模組列印的
  • client_server.log: 同時記錄AppiumBoosterAppium框架的日誌,相當於appium_server.logappium_booster.log的並集,優點在於可以清晰地看到測試用例執行過程中Client端和Server端的通訊互動過程

另外,當測試用例執行失敗時,AppiumBooster會將執行失敗的步驟截圖和日誌提取出來,單獨儲存到errors資料夾中,方便問題追溯。

具體地,每次執行測試前,AppiumBooster會在指定的results目錄下建立一個以當前時間(%Y-%m-%d_%H:%M:%S)命名的資料夾,儲存結構如下所示。

2016-08-28_16:28:48
├── appium_server.log
├── appium_booster.log
├── client_server.log
├── errors
│   ├── 16_31_29_btnLogin.click.dom
│   ├── 16_31_29_btnLogin.click.png
│   ├── 16_32_03_btnMenuMyAccount.click.dom
│   └── 16_32_03_btnMenuMyAccount.click.png
├── screenshots
│   ├── 16_30_34_tablecellMyAccountLogin.click.png
│   ├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.png
│   ├── 16_30_48_sectxtfieldPassword.type_123456.png
│   ├── 16_31_29_btnLogin.click.png
│   └── 16_32_03_btnMenuMyAccount.click.png
└── xmls
    ├── 16_30_34_tablecellMyAccountLogin.click.dom
    ├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.dom
    ├── 16_30_48_sectxtfieldPassword.type_123456.dom
    ├── 16_31_29_btnLogin.click.dom
    └── 16_32_03_btnMenuMyAccount.click.dom複製程式碼

對於每一個測試步驟的截圖和DOM,儲存檔案命名格式為%H_%M_%S_ControlID.ControlAction。採用這種命名方式有兩個好處:

  • 檔案通過時間排序,對應著測試用例執行的步驟順序
  • 可以在截圖或DOM中直觀地看到每一步操作指令對應的執行結果

環境初始化

Appium Server

在執行自動化測試時,某些情況下可能會造成Appium Server出現異常情況(e.g. 500 error),並影響到下一次測試的執行。

為了避免這類情況,AppiumBooster在每次執行測試前,會強制性地對Appium Server進行重啟。方式也比較簡單暴力,執行測試之前先檢查系統是否有bin/appium的程式在執行,如果有,則先kill掉該程式,然後再啟動Appium Server

需要說明的是,由於Appium Server的啟動需要一定時間,為了防止執行Appium ClientAppium Server還未初始化完畢,因此啟動Appium Server後最好能等待一段時間(e.g. sleep 10s)。

iOS/Android模擬器

在模擬器中執行一段時間後,也會存在快取資料和檔案,可能會對下一次測試造成影響。

為了避免這類情況,AppiumBooster在每次執行測試前,會先刪除已存在的模擬器,然後再用指定的模擬器配置建立新的模擬器。

對於iOS模擬器,AppiumBooster通過呼叫xcrun simctl命令的方式來對模擬器進行操作,基本原理如下所示。

# delete iOS simulator: xcrun simctl delete device_id
$ xcrun simctl delete F2F53866-50A5-4E0F-B164-5AC1702AD1BD
# create iOS simulator: xcrun simctl create device_type device_type_id runtime_id
$ xcrun simctl create `iPhone 5` `com.apple.CoreSimulator.SimDeviceType.iPhone-5` `com.apple.CoreSimulator.SimRuntime.iOS-9-3`複製程式碼

其中,device_id/device_type_id/runtime_id這些屬性值可以通過執行xcrun simctl list命令獲取得到。

$ xcrun simctl list
== Device Types ==
iPhone 5s (com.apple.CoreSimulator.SimDeviceType.iPhone-5s)
iPhone 6 (com.apple.CoreSimulator.SimDeviceType.iPhone-6)
== Runtimes ==
iOS 8.4 (8.4 - 12H141) (com.apple.CoreSimulator.SimRuntime.iOS-8-4)
iOS 9.3 (9.3 - 13E230) (com.apple.CoreSimulator.SimRuntime.iOS-9-3)
== Devices ==
-- iOS 8.4 --
    iPhone 5s (E1BD9CC5-8E95-408F-849C-B0C6A44D669A) (Shutdown)
-- iOS 9.3 --
    iPhone 5s (BAFEFBE1-3ADB-45C4-9C4E-E3791D260524) (Shutdown)
    iPhone 6 (F23B3F85-7B65-4999-9F1C-80111783F5A5) (Shutdown)
== Device Pairs ==複製程式碼

增強特性

除了以上基礎特性,AppiumBooster還支援一些輔助特性,可以增強測試框架的使用體驗。

Data引數化

在某些場景下,測試用例執行時需要動態獲取數值。例如,註冊賬號的測試用例中,每次執行測試用例時需要保證使用者名稱為未註冊的,常見的做法就是在註冊使用者名稱中包含時間戳。

AppiumBooster的做法是,可以在測試步驟的data欄位中,傳入Ruby表示式,格式為${ruby_expression}。在執行測試用例時,會先對ruby_expression進行eval計算,然後用計算得到的值作為實際引數。

回到剛才的註冊賬號測試用例,填寫使用者名稱的步驟就可以按照如下形式指定引數。

input test EmailAddress:
  control_id: txtfieldEmailAddress
  control_action: type
  data: ${Time.now.to_i}@debugtalk.com
  expectation: sectxtfieldPassword複製程式碼

實際執行測試用例時,data就會引數化為1471318368@debugtalk.com的形式。

全域性引數配置

對於某些配置引數,例如系統的登入賬號密碼等,雖然可以直接填寫到測試用例的steps中,但是終究不夠靈活。特別是當存在多個測試用例引用同一個引數時,涉及到引數改動時就需要同時修改多個地方。

更好的做法是,將此類引數提取出來,在統一的地方進行配置。在AppiumBooster中,可以在config.yml檔案中配置全域性引數。

---
TestEnvAccount:
  UserName: test@debugtalk.com
  Password: 123456

ProductionEnvAccount:
  UserName: production@debugtalk.com
  Password: 12345678複製程式碼

然後,在測試用例的steps就可以採用如下形式對全域性引數進行引用。

---
AccountSteps:
  input test EmailAddress:
    control_id: txtfieldEmailAddress
    control_action: type
    data: ${config.TestEnvAccount.UserName}
    expectation: sectxtfieldPassword

  input test Password:
    control_id: sectxtfieldPassword
    control_action: type
    data: ${config.TestEnvAccount.Password}
    expectation: btnLogin複製程式碼

optional選項

在執行測試用例時,有時候可能會存在這樣的場景:某個步驟作為非必要步驟,當其執行失敗時,我們並不想將測試用例判定為不通過。

基於該場景,在測試用例設計表格中增加了optional引數。該引數值預設不用填寫。但如果在某個步驟對應的optional欄填寫了true值後,那麼該步驟就會作為非必要步驟,其執行結果不會影響整個用例的執行結果。

例如,在電商類APP中,某些賬號有優惠券,登入系統後,會彈出優惠券的提示框;而有的賬號沒有優惠券,登入後就不會有這樣的彈框。那麼關閉優惠券彈框的步驟就可以將其optional引數設定為true。

---
AccountSteps:
  close coupon popup window(optional):
    control_id: btnClose
    control_action: click
    expectation: !btnViewMyCoupons
    optional: true複製程式碼

命令列工具

AppiumBooster通過在命令列中進行呼叫。

$ ruby start.rb -h
Usage: start.rb [options]
    -p, --app_path <value>           Specify app path
    -t, --app_type <value>           Specify app type, ios or android
    -f, --testcase_file <value>      Specify testcase file(s)
    -d, --output_folder <value>      Specify output folder
    -c, --convert_type <value>       Specify testcase converter, yaml2csv or csv2yaml
        --disable_output_color       Disable output color複製程式碼

執行測試用例

指定執行測試用例時支援多種方式,常見的幾種使用方式示例如下:

$ cd ${AppiumBooster}
# 執行指定的測試用例檔案(絕對路徑)
$ ruby run.rb -p "ios/app/test.zip" -f "/Users/Leo/MyProjects/AppiumBooster/ios/testcases/login.yml"

# 執行指定的測試用例檔案(相對路徑)
$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/login.yml"

# 執行所有yaml格式的測試用例檔案
$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/*.yml"

# 執行ios目錄下所有csv格式的測試用例檔案
$ ruby run.rb -p "ios/app/test.zip" -t "ios" -f "*.csv"複製程式碼

測試用例轉換

將YAML格式的測試用例轉換為CSV格式的測試用例:

$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.yml複製程式碼

總結

什麼才算是心目中理想的自動化測試框架?我也沒有確切的答案。

為什麼要登山?
因為山在那裡。


原文連結:debugtalk.com/post/build-…

專案原始碼:github.com/debugtalk/A…


關於作者

筆名九毫,英文名Leo Lee。

專注於軟體測試領域和測試開發技術,享受在牆角安靜地debug,也喜歡在部落格上分享文字。

個人部落格:debugtalk.com

個人微信公眾號:DebugTalk

DebugTalk

相關文章