客戶端自動化測試研究
背景
測試作為質量保證極其重要的一環,在移動App開發流程中起到非常關鍵的作用。從開發工程師到測試工程師,人人都應具備良好的測試意識,將隱患和風險在上線之前找出並解決,可以有效的減少線上事故。
美團和大眾點評App作為美團點評平臺的主要入口,支援承載著美團點評各大業務。其中美團點評境外度假業務主要包括了出境遊相關業務以及所有的境外城市站,也是美團點評非常看重和大力發展的業務線。為了保證質量,需要進行各項測試:冒煙測試[1]、功能測試、整合測試、專項效能測試,迴歸測試[2]。其中冒煙測試和迴歸測試大多由開發自己手動執行,有較大的優化空間。一方面,測試的人力成本較高;另一方面,在之前的測試過程中發生過漏測等問題,這些問題在測試階段被QA發現,又會再次返工,費時費力。
鑑於這兩部分測試用例相對穩定,不會頻繁發生較大的變化,我們打算將其自動化,降低人力成本投入,將測試結果報表化,避免人為疏漏造成的一系列問題。
[1]冒煙測試(smoke testing),就是開發人員在個人版本的軟體上執行目前的冒煙測試專案,確定新的程式程式碼不出故障。冒煙測試的物件是每一個新編譯的需要正式測試的軟體版本,目的是確認軟體基本功能正常,可以進行後續的正式測試工作。冒煙測試的執行者是版本編譯人員。 [2]迴歸測試是軟體測試的一種,旨在檢驗軟體原有功能在修改後是否保持完整。
方案選型
目前業界測試方案非常多,Android和iOS雙平臺的方案加起來大約有十七八種。應該如何選擇適合團隊的測試方案呢?我們主要考慮以下幾個方面:
- 平臺支援。
- 穩定性。
- 維護成本。
- 可擴充套件性。
其中維護成本我們尤為看重。目前團隊的開發和測試同學任務都比較飽和,業務處於高速發展期,沒法抽出太多的時間開發/維護測試指令碼,這就需要在這方面做到在投入較少時間的前提下不影響自動化測試的結果產出。常規的TDD[3]是函式級別進行測試驅動開發,通常需要在程式碼級別做很多工作,需要測試團隊投入較大的開發成本。鑑於在成本方面的考慮,我們打算使用BDD[4]來解決這個問題。主要在行為層面進行測試投入,在程式碼層級方面投入較小,用非常有辨識力的行為進行測試。
在平臺支援方面,由於是客戶端團隊,所以我們希望寫好的用例可以同時跑在Android和iOS兩個平臺上,還希望用例可以一部分進行美團和大眾點評兩個App的複用,所以需要一個可以跨平臺的方案。
[3]測試驅動開發(Test-driven development,縮寫為TDD)是一種軟體開發過程中的應用方法,倡導先寫測試程式,然後編碼實現其功能得名。測試驅動開發是戴兩頂帽子思考的開發方式:先戴上實現功能的帽子,在測試的輔助下,快速實現其功能;再戴上重構的帽子,在測試的保護下,通過去除冗餘的程式碼,提高程式碼質量。 [4]行為驅動開發(Behavior-driven development,縮寫BDD)是一種敏捷軟體開發的技術。它通過用自然語言書寫非程式設計師可讀的測試用例擴充套件了測試驅動開發方法。
從入門到放棄
去年年底的時候我們團隊就自動化測試方面進行了探索。發現Calabash滿足BDD和跨平臺,於是進行了小範圍試用。在指令碼開發和維護方面,成本確實低於函式級別的測試開發,它可以用一種類似自然語言的方式編寫測試用例,這是一個簡單的test case示例:
Scenario: 首頁
Then I press "上海"
When I press view with id "city"
Then I see "海外"
When I press "海外"
And I press view with id "start_search"
When I enter "東京" into input field number 1
Then I press list item number 1
Then I see "東京"
When I press "美食"
...
這個示例相信開發工程師們甚至沒寫過程式碼的人也看得懂,其實就是用常規的行為思維模式去編寫測試用例。其中Feature、Scenario、Step是BDD的三個核心概念:
- Feature:就是字面意思,主要是描述功能特性。
- Scenario:場景,在這裡可以簡單的理解為一個個的細分case,通常情況下需要多個場景拼接來完成一個具體的test case。
- Step:實現場景的步驟程式碼。
但是Calabash在業內相對小眾,遇到問題就不太好解決。比如在某些三星手機上就遇到了某些控制元件根據ID找不到的問題,會影響UI元素的定位。在編寫自動化指令碼時,元素定位的唯一性是一個看似簡單實際上會有很多坑的問題,指令碼的穩定性一定程度上依賴瞭如何進行元素定位。
其次,在Android團隊想要把方案推廣到iOS平臺的時候,我們發現了一個很大的問題:iOS接入Calabash的成本太高。Android的接入成本很低,只需要一個重簽名的apk檔案就可以了,並不依賴原始碼,而iOS的接入需要依賴原始碼做一些工作,這就給iOS同學造成了很多困難。美團和大眾點評是兩個巨大的App,在原始碼接入方面的工作量並不小,而且很多隱患無法預料,就算依賴原始碼接入之後,還有一個問題需要解決:iOS的ID系統。通常iOS業務開發程式碼中不是通過ID來獲取頁面元素,不管是手寫佈局程式碼還是用xib佈局,開發者一般不會給介面元素加ID,所以iOS的元素大多都沒有ID,而Calabash對元素的定位主要依賴ID,這無疑讓我們感到雪上加霜。
在Android團隊用寫好的用例進行了幾個版本的冒煙測試之後,團隊內部Android、iOS、QA的同學坐下來一起進行了方案後續的探究,最終決定放棄Calabash,繼續尋找可以替代的方案。
從放棄到再次擁抱
在經歷過Calabash的挫折之後,我們在選型方面更加慎重。QA同學對Appium有一定的經驗,於是先採用了Appium方案進行相容性測試和部分迴歸測試。在業務快速發展的過程中,維護成本讓QA同學越來越疲於應付,於是我們又坐在一起進行新方案的討論和探索。
Calabash的BDD模式是大家認可的,也是大家願意接受的,那就需要在新的方案中,繼續使用這種方式編寫維護測試用例。我們想把Appium和Calabash兩者的優勢結合起來,還想把之前寫過的Calabash的測試用例無縫遷移繼續使用。
取其精華
Calabash為什麼可以使用類似自然語言的方式編寫測試用例達到BDD的效果呢?根本原因是因為Cucumber。
在Calabash官網中註明了他們使用了Cucumber(一種簡單的自然語言方式的BDD開源解決方案),那麼我們能否底層使用Appium支援,上層使用Cucumber進行測試用例的開發和維護呢?
答案當然是可行的。我們在Appium的官方示例程式碼中找到了答案。Appium官方提供了與Cucumber結合使用的例子作為參考,雖然這部分程式碼已經兩年沒更新了,但是依然給我們提供了關鍵思路。
新方案形成
客戶端的同學與QA同學進行了討論,確認了使用QA同學目前使用的按照App進行用例拆分的方案。之前Calabash的方案有很多可以借鑑過來,於是我們先進行了整體結構的調整:
按照點評和美團兩個App進行用例區分,公共步驟的封裝在common_steps.rb中。點評和美團的目錄下分別有cucumber.yml指令碼,這是用來區分Android和iOS平臺的,內容大概是這樣:
# config/cucumber.yml
##YAML Template
---
ios: IDEVICENAME='ios'
android: IDEVICENAME='android'
其中Android/config和iOS/config是Android和iOS兩個平臺的特定配置,這部分配置程式碼在support包內,是Appium啟動需要載入的配置。
平臺的區分在env.rb中體現出來:
class AppiumWorldendif ENV['IDEVICENAME']=='android'
caps = Appium.load_appium_txt file: File.expand_path("./../android/appium.txt", __FILE__), verbose: trueelsif ENV['IDEVICENAME']=='ios'
caps = Appium.load_appium_txt file: File.expand_path("./../ios/appium.txt", __FILE__), verbose: trueelse
caps = Appium.load_appium_txt file: File.expand_path('./', __FILE__), verbose: trueendAppium::Driver.new(caps)Appium.promote_appium_methods AppiumWorldWorld do
AppiumWorld.newend
這樣通過cucumber -p android/ios就能執行相應平臺的用例了,Cucumber其他引數自行查閱,和Calabash非常相似。
完全移除Calabash之後,所有Calabash內建的Steps就沒有了,需要重新封裝。其中Feature、Scenario、Step的概念沒有發生變化,和Calabash完全一致。重新封裝Steps需要依賴appium_lib。為了降低封裝成本,提供更多可用的Steps,我們還引入了selenium-cucumber作為輔助使用。
最後testdata.rb是儲存測試資料的檔案,例如測試賬號的登入使用者名稱和密碼等資料。
最終需要依賴的庫大致是這些:
gem 'appium_lib', '~> 9.4.2'
gem 'rest-client', '~> 2.0.2'
gem 'rspec', '~> 3.5.0'
gem 'cucumber', '~> 2.4.0'
gem 'rspec-expectations', '~> 3.5.0'
gem 'spec', '~> 5.3.4'
gem 'selenium-cucumber', '~> 3.1.5'
這樣就完成了組合方案的整體框架。
新方案優勢
新方案形成之後,我們的提測流程就多了一道保障:
於是每個客戶端RD都可以愉快的點選指令碼生成測試報告,提交給QA同學,省去了大家本地跑測試的時間,也幫助QA同學節約了時間,不會再出現返工或者測試遺漏的情況。
整體穩定性提高
由於底層切換到了Appium,穩定性提高了,同樣的機型不再出現類似Calabash的不相容問題了(根據ID無法定位到某個元素),QA同學在Appium的自動化道路上已經做過不少實踐,具有相關經驗。在Webview方面支援也是比較好的,相比Calabash只是多了切換Webview和Native上下文的步驟,Appium的優勢完全體現出來了。
iOS接入成本降低
針對Android和iOS的接入成本,也降低到了一致。Android依舊是提供apk,iOS提供重簽名的ipa包即可,無需原始碼整合,這就解決了Calabash方案iOS整合成本大的問題。
元素定位手段增多
公共Steps一次封裝處處可用,在跨App複用的業務上,測試程式碼也幾乎可以複用,編寫測試指令碼的成本再次降低。iOS控制元件缺少ID不好定位的問題也得到了解決,Appium支援ID、class、name、XPath等元素定位方式,如果前三者都不可用的情況下,使用相對複雜但幾乎萬能的XPath都可以得到解決。
例如一個複雜的XPath:
Then I press view with xpath "//android.widget.LinearLayout[1]
/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]
/android.widget.FrameLayout[1]/android.widget.LinearLayout[2]
/android.widget.FrameLayout[1]/android.widget.ListView[1]
/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]"
不用擔心這麼複雜的XPath應該怎麼寫,這其實是最簡單的,因為可以通過Appium-inspector抓取得到。當然XPath的寫法有很多種,可以選用相容性更好的寫法。
原有指令碼無縫遷移
之前在使用Calabash的時候編寫的指令碼,在封裝好公共Steps之後,幾乎無縫的進行了遷移,對上層編寫測試用例的同學來說,幾乎沒有變化,無需關心是Calabash還是Appium,使用和原先一樣的BDD方式繼續愉快的寫用例就好。
Calabash方案時期的homepage場景(部分):
切換新方案後homepage場景(部分):
並沒有太大的差別。
易整合JenKins,報告視覺化
Cucumber可以進行報表的視覺化輸出,只要在命令後面追加--format html --out reports.html --format pretty,在執行完全部指令碼之後就可以看到生成好的HTML格式的測試報告,也可以使用JSON的格式。
整合Jenkins的方式也相對常規,只要安裝好需要的依賴就可以。
在測試過程中,我們使用了公司內部的雲測機器遠端平臺:
利用遠端平臺的真機進行遠端指令碼測試,測試報告示例如下:
自動化測試執行效果
在境外業務線客戶端進行了自動化測試實踐,目前用於固有冒煙自動化,方案前後對比如下。
Calabash方案時期境外點評固有冒煙用例耗時:
新方案境外點評固有冒煙用例耗時(相比之前Calabash方案時期的用例有所增加):
通過資料對比可以看出,用例數量與執行耗時並不是嚴格的線性關係,在用例數量擴大一倍的情況下,耗時並不會線性的擴大一倍。
開發成本:單個用例的開發成本主要根據用例規模相關,開發一個包含7個動作的用例大概耗時30分鐘左右,其中包括了定位元素的耗時。多個用例的開發成本不止和用例規模相關,還和用例之間是否有複用的場景相關,這就牽扯到了Scenario拆分粒度的問題,下文中有提到。
目前執行用例美團+點評總耗時20分鐘左右,降低了人力成本,避免了QA同學返工的情況,方案新老交替無縫平滑過渡,維護成本低。這不僅是我們團隊對自動化方案的期許,也是自動化測試的價值所在。
問題與展望
問題
scroll or swipe?
在使用UIAutomation的時候,Android頁面滑動採取的方式是呼叫scroll_uiselector方法,例如:
Then /^I scroll to view with text "([^"]*)"$/ do |value|
text = %Q("#{value}")
args = scroll_uiselector("new UiSelector().textContains(#{text})")
find_element :uiautomator, argsend
但是這種方式存在不穩定性因素,在某些情況下,滑動搜尋UI元素非常慢(上下滑動很多次)甚至滑動多次最後仍然搜尋不到,指令碼會執行失敗。在比較複雜的App上很容易出現,是整體指令碼穩定性和成功率的瓶頸。如果更換為UIAutomation2,就可以使用swipe語句進行相對精準的滑動:
swipe start_x: start_x, start_y: start_y, end_x: start_x, end_y: start_y - pixel.to_i
根據撰寫本文時Appium的最新版本v1.6.5進行實踐,發現切換UIAutomation2後使用swipe滑動,對比scroll的方式成功率提高了一倍多,耗時減半,效果非常顯著。雖然其他語句會略微受一點影響,不過整體改動幅度很小,價效比很高,而且UIAutomation2還支援對Toast的識別,整體穩定性大幅提高,建議使用UIAutomation2。
Scenario拆分粒度
在很多情況下,一個test case是由一個或多個Scenario組成的,不同的test case又會存在部分Scenario複用的情況,明確Scenario的拆分粒度可以幫助開發人員降低測試指令碼的編寫成本,達到一定程度上的App內部複用甚至跨App複用。尤其在多人協作的環境下,這是一個非常值得探究的問題。
展望
自動觸發雲測
目前觸發的方式是人工觸發Jenkins job,最後輸出報告。未來要做的是在特定的時期自動觸發job進行雲端自動化,觸發時期可能會參考App的開發週期時間節點。
人人都是測試工程師
我們希望團隊內人人都具備良好的測試思維,能站在測試的角度想問題,領悟測試驅動開發的意義。通過簡單的方式讓團隊內的同學們參與測試,體會測試,寫出更優秀的程式碼。
參考資料