作過自動化測試的人應該都會有這樣一種體會,要寫個自動化demo測試用例很容易,可是要真正將自動化測試落地,對成百上千的自動化測試用例實現較好的可複用性和可維護性就很難了。android
基於這一痛點,我開發了AppiumBooster
框架。顧名思義,AppiumBooster
基於Appium
實現,但更簡單和易於使用;測試人員不用接觸任何代碼,就能夠直接採用簡潔優雅的方式來編寫和維護自動化測試用例。ios
原型開發完畢後,我將其應用在當前所在團隊的項目上,並在使用的過程當中,按照本身心目中理想的自動化測試框架的模樣對其進行迭代優化,最終打磨成了一個本身還算用得順手的自動化測試框架。git
本文即是對AppiumBooster
的核心特性及其設計思想進行介紹。在內容組織上,本文的各個部分相對獨立,你們可直接選擇本身感興趣的部分進行閱讀。github
UI交互是自動化測試的基礎,主要分爲三部份內容:定位控件、操做控件、檢測結果。express
定位控件時,統一採用元素ID進行定位。這裏的ID包括accessibility_id
或accessibility_label
,須要在iOS工程項目中預先進行設置。json
另外,考慮到控件可能出現延遲加載的狀況,定位控件時統一執行wait
操做;定位成功後會當即返回控件對象,定位失敗時會進行等待並不斷嘗試定位,直到超時(30秒)後拋出異常。緩存
wait { id control_id }複製代碼
源碼路徑:AppiumBooster/lib/pages/control.rb
ruby
根據實踐證實,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
對於自動化測試而言,自動化測試用例的組織與管理是最爲重要的部分,直接關係到自動化測試用例的可複用性和可維護性。
通過綜合考慮,AppiumBooster
從三個層面來描述測試用例,從低到高分別是step
、feature
和testcase
;描述方式推薦使用YAML
格式。
首先是對於單一操做步驟的描述。
從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時,當前步驟的執行結果不影響整個測試用例。
各個模塊的單一操做步驟定義完畢後,雖然能夠直接將多個步驟進行組合造成對測試場景的描述,即測試用例,可是操做起來會過於侷限細節;特別是當測試用例較多時,可維護性是一個很大的問題。
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模塊名稱和操做步驟的名稱,並以|
進行分隔。
在功能點描述的基礎上,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複製代碼
基本上,YAML
測試用例引擎已經能夠很好地知足組織和管理自動化測試用例的需求。
但考慮到部分用戶會偏向於使用表格的形式,由於表格看上去更直觀一些,AppiumBooster
同時還支持CSV
格式的測試用例引擎。
採用表格來編寫測試用例時,只須要在任意表格工具,包括Microsoft Excel、iWork Numbers、WPS等,按照以下形式對測試用例進行描述。
而後,將表格內容另存爲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
已實現的記錄形式有以下三種:
因爲Appium
分爲Server端和Client端,所以AppiumBooster
在記錄日誌的時候也將日誌分爲了三份:
appium_server.log
: Appium Server端的日誌,這部分日誌是由Appium框架
打印的appium_booster.log
: 包括測試環境初始化和測試用例執行記錄,這部分日誌是由AppiumBooster
中採用logger模塊打印的client_server.log
: 同時記錄AppiumBooster
和Appium框架
的日誌,至關於appium_server.log
和appium_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
。採用這種命名方式有兩個好處:
在執行自動化測試時,某些狀況下可能會形成Appium Server
出現異常狀況(e.g. 500 error),並影響到下一次測試的執行。
爲了不這類狀況,AppiumBooster
在每次執行測試前,會強制性地對Appium Server
進行重啓。方式也比較簡單暴力,運行測試以前先檢查系統是否有bin/appium
的進程在運行,若是有,則先kill掉該進程,而後再啓動Appium Server
。
須要說明的是,因爲Appium Server
的啓動須要必定時間,爲了防止運行Appium Client
時Appium 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
還支持一些輔助特性,能夠加強測試框架的使用體驗。
在某些場景下,測試用例執行時須要動態獲取數值。例如,註冊帳號的測試用例中,每次執行測試用例時須要保證用戶名爲未註冊的,常見的作法就是在註冊用戶名中包含時間戳。
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欄填寫了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-…
筆名九毫,英文名Leo Lee。
專一於軟件測試領域和測試開發技術,享受在牆角安靜地debug,也喜歡在博客上分享文字。
我的博客:debugtalk.com
我的微信公衆號:DebugTalk