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

前言

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

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

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

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

UI交互基礎

UI交互是自動化測試的基礎,主要分爲三部份內容:定位控件、操做控件、檢測結果。express

控件定位

定位控件時,統一採用元素ID進行定位。這裏的ID包括accessibility_idaccessibility_label,須要在iOS工程項目中預先進行設置。json

另外,考慮到控件可能出現延遲加載的狀況,定位控件時統一執行wait操做;定位成功後會當即返回控件對象,定位失敗時會進行等待並不斷嘗試定位,直到超時(30秒)後拋出異常。緩存

wait { id control_id }複製代碼

源碼路徑:AppiumBooster/lib/pages/control.rbruby

控件操做

根據實踐證實,UI的控件操做基本主要就是點擊、輸入和滑動,這三個操做基本上能夠覆蓋絕大多數場景。bash

  • 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

相關文章
相關標籤/搜索