隨著Android技術的發展各種開源庫層出不窮,開發一個Android應用已經變得容易了很多。然而開發一個商業應用并不是單純是實現業務需求那么簡單,開發完成只是基礎,后續還需要經過QA同學的嚴格測試。然而對于小型創業公司來說,我們并沒有BAT等大廠里的測試平臺、方案研究員,我們QA資源比較有限,如果將一切發現問題的重擔都交給測試部門,不但耗費的測試周期長,而且有一些問題將難以發現。例如某個crash只會在某個場景下復現,某個內存泄漏只有在用戶執行了某個操作才會出現,而QA同學在測試時并不一定能夠執行到那條crash的測試路徑。對于內存泄漏來說,即使測試到了那條路徑,但可能他們并不是在測試內存問題,因此即使出現了內存泄漏也難以發現。然而由于內存泄漏導致的OOM、空指針正是導致應用崩潰的兩大原因,因此盡早的發現并且解決掉這類問題對于應用質量來說至關重要。
也許有同學會說通過LeakCanary可以很方便的為我們檢測內存泄漏,但是問題是我們并不能保證我的研發、QA同學在每個版本都會通過LeakCanaey檢測各個頁面的內存問題,因為人不是機器,你不能保證每一次都會進行手動回歸。而如果在開發中直接引入LeakCanary會拖慢你的開發速度。因此,找到一種低成本、高收益的自動化測試方案來保證應用的穩定性對于創業小團隊來說還是非常有價值的。
這篇文章我就來分享一下我們是如何保證應用的穩定性、避免內存泄漏的。首先我列一下幾個要點:
Jenkins 持續集成
單元測試
Monkey 壓力測試 以及 log收集
定制 LeakCanary 實現配合Monkey測試的內存檢測
在敏捷方法中,持續集成是其基石,持續集成的核心是自動化測試。Jenkins是一個可擴展的持續集成平臺,它提供了豐富的插件能夠讓開發人員完成各種任務。它主要作用有如下兩個方面:
持續、自動地構建或者測試軟件項目;
定時地執行任務。
對于Android項目來說,你可以理解為它可以定期的拉取代碼,然后打包你的應用,并且執行一些特定的任務,例如打包之后運行單元測試、壓力測試、UI自動化測試、上傳到fir.im 上等。Jenkins的執行流程大致如圖 1-1 所示 :
圖 1-1
通過定時觸發Jenkins構建任務,它能夠自動從github拉取代碼、打包apk、運行我們的測試任務,后我們可以將結果通過郵件發送給相關人員。例如我們的Jenkins每隔兩個小時就會執行一次單元測試(如果代碼有改動),然后將結果發送給相關人員。假如有一位同事進行了代碼重構,但是引入了錯誤,那么單元測試將會快速的發現問題,并且后通過郵件將報告發送給相關人員。相關人員通過報告發現錯誤之后就會盡快修復bug, 而不需要等到測試階段經過各種測試路徑之后才能發現問題。如果這個問題在QA測試階段沒有被覆蓋到,那么就會導致有問題apk交付給用戶。
關于如何搭建Jenkins平臺我就不做過多介紹,這方面的資源比較多,大家可以參考下面兩篇文章。
Ubuntu下搭建Android開發環境
搭建Jenkins持續測試平臺
說到自動化測試,成本低的應該是單元測試。單元測試成本低,但是收益卻非常高。因為它是基礎的測試,正所謂“九層之臺,起于壘土;千里之行,始于足下”,只有基礎牢固了才能保證更高層次的正確性。但是由于國內開發人員對于單元測試認識不多,所以能夠寫單元測試的開發人員并不是很多,也正因為如此在2015年我才在《Android開發進階:從小工到專家》的第九章詳細講述了單元測試,也是希望將這些知識盡早的推薦給早期接觸Android開發的同學,因此本文不會再次介紹如何寫單元測試。言歸正傳,這些測試策略其實很早就有總結過,的就是Martin Fowler的測試金字塔,如圖 2-1所示。
Martin Fowler是世界的面向對象分析設計、UML、模式等方面的專家,敏捷開發方法的創始人之一,現為ThoughtWorks公司的首席科學家,出版過《重構:改善既有代碼的設計》、《企業應用架構模式》等名著。
圖 2-1 中將自動測試分為了三個層次,從下到上依次為單元測試、業務邏輯測試、UI測試,越往上測試成本越高、測試的效率越低,也就是說單元測試是整個測試金字塔中投入少、收益高、測試效率高的測試類型。
舉個具體的例子,假如我們的應用中有數據庫緩存功能,那么我們如何快速驗證數據庫存儲模塊是否正確?通常的流程我們是運行應用得到UI上的數據,然后記錄當前的數據,數據存儲之后,然后再重新進入應用,再與之前記錄的數據做對比,反復執行這個過程來來確保數據的正確性。每次發布新版本之前測試人員都得執行上述測試流程,枯燥無味不說,還容易出錯、浪費時間,而如果我們有單元測試,那么我們只需要運行一次單元測試,如果測試通過我們就認為數據庫緩存模塊基本沒有問題,再簡單配合我們的人工測試就可以通過測試,這樣一來效率就提高了很多。
這三個層次的自動化測試的分配比例從下到上通常為 70% 、20%、10%,可見單元測試在整個自動化測試中占據了非常大的比例。通過單元測試,我們能夠獲得如下收益:
便于后期重構。用單元測試盡量覆蓋程序中的每一項功能的正確性,這樣就算是開發后期,也可以有保障地增加功能或者更改程序結構,而不用擔心這個過程中會破壞原來的功能,因為單元測試為代碼的重構提供了保障。只要重構代碼之后單元測試全部運行通過,那么,在很大程度上表示這次重構沒有引入新的Bug,當然這是建立在完整、有效的單元測試覆蓋率的基礎上;
優化設計。編寫單元測試將使用戶從調用者的角度觀察、思考,特別是使用測試驅動開發的開發方式,迫使設計者把程序設計成易于調用和可測試,并且解除軟件中的耦合。
具有回歸性。自動化的單元測試避免了代碼出現回歸,編寫完成之后,可以隨時隨地地快速運行測試。而不是將代碼部署到設備上,然后再手動地覆蓋各種執行路徑,這樣的行為效率低下、浪費時間。
提高你對代碼的信心。通過單元測試,相當于我們從另一個角度審視了我們的代碼,并且驗證了它們的正確性,這樣一來使得我們對于代碼更有信心,而不是在上線之后還擔心基礎代碼會出現問題。
當我們有單元測試之后,我們就可以在Jenkins上執行Gradle任務(需要安裝Gradle插件),以此來執行我們的單元測試。首先需要添加構建步驟,然后選擇”Invoke Gradle Scripts”, 然后在Gradle任務下如圖 2-2 所示的任務:
圖 2-2
配置好之后我們就將Android設備(或者使用模擬器插件)連接到jenkins主機上,然后觸發Jenkins任務啟動單元測試的任務,Jenkins就會執行我們配置的Gradle腳本 assembleDebug connectedDebugAndroidTest --continue 任務,這個任務會打包一個debug版的apk包,然后安裝被測項目、測試項目,后執行工程中的單元測試。如果我們配置了郵件插件,那么我們也可以將測試報告(測試報告存放在 build/reports/androidTests/connected/flavors/測試的flavor/index.html)通過郵件發送給相關人員。如表 2-1 所示:
郵件通知
測試成功
測試失敗
假如測試失敗,那么我們通過測試報告就知道是哪個測試運行失敗,以及為什么失敗,然后相關人員就可以快速的修復bug,將基礎bug扼殺在搖籃之中。
還是回到前文提到的,寫單元測試需要一定的知識,怎么編寫單元測試不是難點,難點是怎么讓你的代碼可以測試,這些涉及到解耦、依賴注入等知識,雖然說很淺顯,但是很多工程師并沒有真正領會到這些,因此能夠寫單元測試的工程師是少之又少。也正是因為這樣,在小公司執行單元測試才會顯得困難。
將基礎的bug扼殺于單元測試后我們還要面臨高層次的測試問題,例如在某些頁面的某些情況下應用會發生崩潰,但是測試的時候我們沒有測試到該場景,因此就上線之后發現某個頁面崩潰直線上升。由于測試資源、測試時間有限,這種情況難以避免,為了盡量避免這種情況我們可以通過Monkey進行壓力測試。
Monkey是一款壓力測試工具,它能夠根據用戶指定的事件比例向指定的應用發送事件,比如觸摸事件、點擊事件、屏幕旋轉等,通過Monkey測試能夠讓應用處在一個未知的測試環境下(通俗點講就是有規律的在應用內亂點),這個時候我們往往能夠發現QA同學沒有測出來的bug,從另一個層面保證應用的質量。
在執行Monkey的過程中,如果應用產生了崩潰、ANR等,它都會輸出日志,測試結束之后如果測試失敗我們只需要查看錯誤日志就可以發現問題所在。通過這種自動化的測試、日志收集,我們就能夠邊開發、邊測試,盡早的發現、修復bug。
要在Jenkins中實現壓力自動化測試,我們需要如下幾步:
通過gradle命令生成apk,并且安裝
執行 monkey 腳本進行測試
獲取并且發送測試報告
生成apk我們可以通過添加gradle 腳本命令實現,方式與圖2-2中一樣,只需要我們將Switches的值修改為”assembleDebug”。然后在Jenkins中我們可以為一個項目添加構建任務,任務類型為 “Execute Shell”, 如圖 3-1 所示:
圖 3-1
Execute Shell中的內容就是我們要執行的腳本,作用分別為:
unlock.sh - 設備解鎖,然后才可以讓Monkey運行下一步的壓力測試。
啟動真正的壓力測試, 即執行 start_monkey.sh 腳本;
分析測試日志,判定測試的成功與失敗;
其中start_monkey.sh為重要,核心腳本如下所示:
上述腳本(需要根據情況替換掉部分內容)的含義為執行 100000次 事件,每次事件相隔 500毫秒,忽略崩潰、忽略ANR,--pct-touch 40 --pct-motion 25 --pct-appswitch 10 --pct-rotation 5 為設定各種事件的百分比,Monkey的具體參數這里不再贅述,大家可以查看其他文章。
在執行這100000次事件的過程中,如果出現ANR、crash,那么相關的日志會輸出到 $project/test_logs/monkey_error.txt 路徑中,當測試結束之后我們可以判定monkey_error.txt文件的大小,如果monkey_error.txt文件中有內容那我們則認為本次測試失敗,然后通過郵件將 monkey_error.txt 作為附件發送給相關人員,相關人員就可以通過 monkey_error.txt 以及測試設備中的 /data/anr/traces.txt 文件來定位、修復問題! 重要的是這些操作我們都可以讓Jenkins在夜間自動的為我們來完成,定期執行任務、分析報告與log、發送郵件,例如我們的Jenkins任務會在每天夜里 10點之后執行壓力測試,每次測試跑8個小時,那么在第二天早上我們就可以得到測試報告,如果發現問題我們就可以在早上將問題解決掉,而不會拖到提交測試之后!
如果你的應用能夠經受8個小時壓力測試蹂躪之后沒有崩潰、沒有內存泄漏、沒有OOM,那么在一定的程度上來說你的應用已經具備一定的穩定性。然后問題顯然沒有那么簡單,在執行壓力測試的早期,你很可能在一個連續的時間段內都面臨測試失敗的問題。崩潰問題比較好查找愿意,那如果在壓力測試過程中如果出現了內存泄漏我們怎么知道呢?我們有沒有辦法能夠自動化的發現問題?
我們的解決方案是通過定制 LeakCanary 來實現在自動化測試的過程中自動檢測內存泄漏,因為 LeakCanary 默認是在發現內存泄漏是在通知欄顯示,這樣不便于實現自動化。我們通過修改 LeakCanary 發現內存泄漏的策略來實現我們的目標,即發現內存泄漏之后將相關信息寫入到一個具體的文件,然后測試完成之后分析這個文件,如果這個文件里面有內容,那么認為產生了內存泄漏,后將這個log文件通過郵件發送給相關人員。我們的修改如下:
LeakCanary 檢測到內存泄漏之后就會執行 LeakDumpService 中的 onHeapAnalyzed 函數,在這個函數中我們將泄漏的信息保存到一個文件中,每次運行產生的log會疊加寫入到同一個文件,因此如果一次測試產生了多個泄漏我們就從一個文件中得到。要使用LeakDumpService作為LeakCanary發現泄漏后的處理服務需要進行如下配置:
通過調用LeakCanaryForTest的install函數,我們就可以將LeakDumpService作為LeakCanary發現泄漏后的處理服務。這樣一來,我們就可以在執行壓力測試時通過 LeakCanary 檢測內存泄漏,并且將內存泄漏輸出到一個日志文件中,后通過郵件得到這個日志,然后根據日志修復內存泄漏問題。因為壓力測試的事件是隨機性的,因此它能夠發現一些比較隱蔽的問題,這些測試路徑可能我們的QA同學不會測試到,因此Monkey 結合 LeakCanary 往往能夠得到意想不到的效果.
內存泄漏檢測效果如圖 3-2 所示:
圖3-2
2017-03-27_leak.txt就是內存泄漏的日志文件, 部分日志如下所示:
如果你一大早來到公司就收到了內存泄漏測試結果的報告,那么恭喜你,你又即將解決了一個隱蔽的內存問題! 當然,沒有人愿意在一大早打開郵件就看到這類的測試報告。但這又何嘗不是一件好事,通過自動化的手段盡早的發現問題,解決問題,降低了成本、提升了應用質量。經過一段時間之后,我們相信應用內的內存泄漏問題會基本上被消滅掉!
然而,我們并不是在開發的時候將 LeakCanary 引入到我們的工程中,因為它會拖慢我們的編譯速度,在開發測試過程中 LeakCanary 的內存檢測也會導致應用運行卡頓。比如我們只希望在運行壓力測試時引入 LeakCanary 進行內存檢測,那么我們可以新建一個 module (這里我們暫且叫做 leakfortest ), 該模塊引用了 LeakCanary, 然后將 LeakCanaryForTest、LeakDumpService等類封裝到這個模塊中,并且在壓力測試的時候引用它。這樣我們的應用模塊build.gradle就需要做類似如下的修改:
然后我們我的應用代碼中添加如下函數,代碼如下:
然后我們在 Application 類中調用 setupLeakCanary 函數,在該函數中會判定如果這個應用是monkey flavor, 那么就會集成 leakfortest 模塊,并且在通過反射調用了LeakCanaryForTest類的install函數來集成我們定制過的 LeakCanary, 從而達到將內存泄漏的日志輸出到特定文件的效果. 為了實現這個效果,我們只需要將gradle任務中生成apk的命令改為 assembleMonkeyDebug, 然后將生成的apk安裝到設備中,后執行測試即可進行后續的流程。這樣一來,我們就將開發與自動化測試隔離開來了!
通過上述的方案,我們就有了一套簡單、投入低、收益高的自動化測試方案,它們能夠快速的發現基礎模塊的問題、內存泄漏問題,能夠保證應用的穩定性。但是這只能保證應用邏輯在單個設備的穩定性,不同的設備可能會產生一些兼容性的問題。因此,另一個重要的測試就是兼容性測試,確保我們的應用在各種設備上能夠正確的運行。如果條件許可,我們可以借助市場上云測試平臺運行一些monkey測試來驗證應用的兼容性,從而避免兼容性引發的問題。
如果說通過jenkins、monkey、單元測試能夠在一個點的角度保證應用的穩定性,那么兼容性測試就是從一個面的角度保證了應用的兼容性。通過這兩個維度的測試,我們的應用肯定會越來越穩定,我們也能從中領悟更多軟件設計、測試的方法與思想。
然而,這一切只是開始,如果團隊有精力和時間,我們還可以在Jenkins中添加更多的方案進行測試。例如:
通過 TinyDancer 、BlockCanary等性能檢測框架來查找性能問題;
在測試過程中定期的輸出內存、CPU占用,測試結束得到一個報表,終可以與其他報告一塊來分析問題;
通過 Espresso、Robotium實現UI自動化測試。
通過不斷的完善自動化平臺,以機器替代部分的人工測試,我們的應用質量將會得到很大程度的保障。即使只有單元測試、壓力測試、LeakCanary內存檢測、云平臺的兼容性測試,我們的應用也能夠經受住創業公司快速迭代帶來的質量考驗。但并不是有更多的測試就會更好,有的時候也會適得其反,因此運用哪些測試方案、做到什么程度都需要根據各自的情況進行決策。我們的目標是提高應用的質量,而不是增加測試的數量。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。