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

前言

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

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

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

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

UI交互基礎

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

控制項定位

定位控制項時,統一採用元素ID進行定位。這裡的ID包括accessibility_id或accessibility_label,需要在iOS工程項目中預先進行設置。

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

wait { id control_id }n

源碼路徑: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從三個層面來描述測試用例,從低到高分別是step、feature和testcase;描述方式推薦使用YAML格式。

steps(測試步驟描述)

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

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

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

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

---nAccountSteps:n enter Login page:n control_id: tablecellMyAccountLoginn control_action: clickn expectation: btnForgetPasswordnn input test EmailAddress:n control_id: txtfieldEmailAddressn control_action: typen data: leo.lee@debugtalk.comn expectation: sectxtfieldPasswordnn check if coupon popup window exists(optional):n control_id: inner_screenn control_action: has_controln data: btnViewMyCouponsn expectation: btnClosen optional: truen

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

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

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

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

features(功能點描述)

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

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

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

---nAccountFeatures:n login with valid test account:n - AccountSteps | enter My Account pagen - AccountSteps | enter Login pagen - AccountSteps | input test EmailAddressn - AccountSteps | input test Passwordn - AccountSteps | loginn - AccountSteps | close coupon popup window(optional)nn login with valid production account:n - AccountSteps | enter My Account pagen - AccountSteps | enter Login pagen - AccountSteps | input production EmailAddressn - AccountSteps | input production Passwordn - AccountSteps | loginn - AccountSteps | close coupon popup window(optional)nn logout:n - AccountSteps | enter My Account pagen - SettingsSteps | enter Settings pagen - AccountSteps | logoutn

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

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

testcases(測試用例描述)

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

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

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

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

---nBuy Phantom 4:n - SettingsFeatures | initialize first startupn - SettingsFeatures | Change Country to Chinan - AccountFeatures | login with valid accountn - AccountFeatures | Change Shipping Address to Chinan - StoreFeatures | add phantom 4 to cartn - StoreFeatures | finish ordern - AccountFeatures | logoutn

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

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

---nSend random text messages:n - SettingsFeatures | initialize first startupn - AccountFeatures | login with valid test accountn - MessageFeatures | enter follower user message pagen - MessageFeatures | send random text message | 100n

測試用例引擎(CSV)

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

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

testcases(測試用例描述)

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

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

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

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

統一轉換後的數據結構如下所示:

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

測試用例轉換器(yaml2csv)

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

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

$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.ymln

過程記錄及結果存儲

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

過程記錄方式

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

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

測試結果存儲

由於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:48n├── appium_server.logn├── appium_booster.logn├── client_server.logn├── errorsn│ ├── 16_31_29_btnLogin.click.domn│ ├── 16_31_29_btnLogin.click.pngn│ ├── 16_32_03_btnMenuMyAccount.click.domn│ └── 16_32_03_btnMenuMyAccount.click.pngn├── screenshotsn│ ├── 16_30_34_tablecellMyAccountLogin.click.pngn│ ├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.pngn│ ├── 16_30_48_sectxtfieldPassword.type_123456.pngn│ ├── 16_31_29_btnLogin.click.pngn│ └── 16_32_03_btnMenuMyAccount.click.pngn└── xmlsn ├── 16_30_34_tablecellMyAccountLogin.click.domn ├── 16_30_41_txtfieldEmailAddress.type_leo.lee@debugtalk.com.domn ├── 16_30_48_sectxtfieldPassword.type_123456.domn ├── 16_31_29_btnLogin.click.domn └── 16_32_03_btnMenuMyAccount.click.domn

對於每一個測試步驟的截圖和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 Client時Appium Server還未初始化完畢,因此啟動Appium Server後最好能等待一段時間(e.g. sleep 10s)。

iOS/Android模擬器

在模擬器中運行一段時間後,也會存在緩存數據和文件,可能會對下一次測試造成影響。

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

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

# delete iOS simulator: xcrun simctl delete device_idn$ xcrun simctl delete F2F53866-50A5-4E0F-B164-5AC1702AD1BDn# create iOS simulator: xcrun simctl create device_type device_type_id runtime_idn$ xcrun simctl create iPhone 5 com.apple.CoreSimulator.SimDeviceType.iPhone-5 com.apple.CoreSimulator.SimRuntime.iOS-9-3n

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

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

增強特性

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

Data參數化

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

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

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

input test EmailAddress:n control_id: txtfieldEmailAddressn control_action: typen data: ${Time.now.to_i}@debugtalk.comn expectation: sectxtfieldPasswordn

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

全局參數配置

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

更好的做法是,將此類參數提取出來,在統一的地方進行配置。在AppiumBooster中,可以在config.yml文件中配置全局參數。

---nTestEnvAccount:n UserName: test@debugtalk.comn Password: 123456nnProductionEnvAccount:n UserName: production@debugtalk.comn Password: 12345678n

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

---nAccountSteps:n input test EmailAddress:n control_id: txtfieldEmailAddressn control_action: typen data: ${config.TestEnvAccount.UserName}n expectation: sectxtfieldPasswordnn input test Password:n control_id: sectxtfieldPasswordn control_action: typen data: ${config.TestEnvAccount.Password}n expectation: btnLoginn

optional選項

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

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

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

---nAccountSteps:n close coupon popup window(optional):n control_id: btnClosen control_action: clickn expectation: !btnViewMyCouponsn optional: truen

命令行工具

AppiumBooster通過在命令行中進行調用。

$ ruby start.rb -hnUsage: start.rb [options]n -p, --app_path <value> Specify app pathn -t, --app_type <value> Specify app type, ios or androidn -f, --testcase_file <value> Specify testcase file(s)n -d, --output_folder <value> Specify output foldern -c, --convert_type <value> Specify testcase converter, yaml2csv or csv2yamln --disable_output_color Disable output colorn

執行測試用例

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

$ cd ${AppiumBooster}n# 執行指定的測試用例文件(絕對路徑)n$ ruby run.rb -p "ios/app/test.zip" -f "/Users/Leo/MyProjects/AppiumBooster/ios/testcases/login.yml"nn# 執行指定的測試用例文件(相對路徑)n$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/login.yml"nn# 執行所有yaml格式的測試用例文件n$ ruby run.rb -p "ios/app/test.zip" -f "ios/testcases/*.yml"nn# 執行ios目錄下所有csv格式的測試用例文件n$ ruby run.rb -p "ios/app/test.zip" -t "ios" -f "*.csv"n

測試用例轉換

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

$ ruby start.rb -c "yaml2csv" -f ios/testcases/login_and_logout.ymln

總結

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

為什麼要登山?

因為山在那裡。

原文鏈接:debugtalk.com/post/buil

項目源碼:github.com/debugtalk/Ap

推薦閱讀:

Atomic Red Team:針對安防設計的新型自動化測試框架
Web自動化測試環境搭建之Python+Selenium
用Tomcat,部署時對server.xml文件里的埠號進行修改,但這三個埠號必須全部修改嗎?
從0到1搭建移動App功能自動化測試平台(0)背景介紹和平台規劃

TAG:自动化测试 | iOS测试 | 开源程序 |