關於 Android 單元測試

Cavabiao發表於2018-06-28

本文不會用各種高大上的理由試圖去說服你寫單元測試,只是描述筆者在單元測試這條路上一路走來的思考和簡單的示例,如果順便能讓你覺得單元測試其實也沒那麼遙遠、回頭也在實際專案中嘗試一下,估計就是本文最大的收穫了。

一、提起單元測試,你對它的映像是什麼?

大部分同學,可能都不瞭解單元測試,在實際專案中覺得這根本就是在浪費時間:我擼程式碼快的飛起,擼完交給 QA 測試就好,沒有必要、也不用做單元測試,真的很多餘,況且專案中一向都是這麼幹沒發現啥問題。

另一部分同學,多少聽說過或者稍微瞭解過單元測試,認為單元測試或許很重要但其實不知道重要在什麼地方,當然也不知道怎麼去寫、到底哪些部分要寫單元測試。

最後,差點漏了已經把單元測試應用在實際專案中且駕輕就熟的同學,為你們鼓掌!嗯,這篇文章不太適合你們。

筆者就是經歷過從不瞭解到了解再到應用的過程。

剛開始工作那一兩年幹前端,因為不是科班出身,根本沒怎麼聽過單元測試,後來開源專案見多了(基本上是國外的),才知道還有單元測試這個東西,但覺得離自己很遙遠,團隊也沒有要求寫。

隨著對 TDD 等概念的瞭解,特別是轉型移動開發後,官方開發者網站上都有單獨的篇幅重點介紹怎麼測試(當然也包括了單元測試),越發覺得它或許很重要,但並沒有深入的理解,當然也不知道我們到底為什麼要寫單元測試,看上去還增加了額外的工作量。

當專案越做越大,開發者、程式碼量也越來越多,慢慢就會暴露出一些問題,也是筆者在實際專案中遇到的痛點(若站在團隊的高度考慮問題,你對筆者的這些痛點會更有同感)。

第一個痛點是人,為什麼這麼說呢?因為人是不靠譜的,如果你花一下午一邊喝咖啡一邊寫完了一個功能模組,一跑 0 bug,包括各種邊界和異常你都全部考慮到了,那你是大神。但飛機也有失誤的時候,更何況大神呢,大神也有狀態不好的時候,所以,是人寫出來的程式碼他都會有 bug,我們的目的是如何去減少它,保證程式碼質量。我們現在都是寄希望於 QA,整個週期太長,想找個 QA/開發坐在你旁邊結對程式設計吧,代價太高別人還不願意!而單元測試恰恰可以幫助我們做到這點,它就像是一個趟在硬碟裡的 QA 機器人,隨叫隨到,實時提供質量保障服務,想想都有點高大上!

第二個痛點是編譯,我想 Android 開發深有體會,編譯時間太長了,修改一個 bug 後執行,打完水回來還在 building。如果只是小問題,這麼來來回回太浪費時間了,時間就是生命啊!

第三個痛點是邊界,有些邊界在真機測試中是很難構造的,藉助單元測試可以突破條件的限制,做為一個有經驗且略帶強迫症的開發我們理應不漏掉任何邊界。

第四個痛點是重構,隨著程式碼質量意識逐漸提升擼程式碼功力也在不斷加強,業務也在不斷髮展變化,其實你會發現不少可以重構的模組,想對某段程式碼下手,又因為不知道影響邊界而放棄。好不容易改了吧,其實心裡是沒有底的,都要 QA 去測試,但 QA 做的是黑盒測試,有一些異常情況我們開發很容易想出來但是 QA 很難測到,無形中增加了心理負擔。

為了解決以上痛點,筆者開始瞭解 Android 單元測試,並運用在實際專案中。

二、什麼是 Android 單元測試?

單元測試就是針對程式最小單元進行正確性校驗的工作^ref1。以 OOP 為例,OOP 中最小單元就是方法,單元測試就是對方法的測試。通俗點講就是,我寫了一堆方法,需要自己保證每個方法的輸入輸出是正確的。

來看看 Android 官方的測試金字塔^ref2

App 測試金字塔

它把 Android App 所涉及到的測試分成了三類,從下往上分別是單元測試,整合測試和 UI 測試。

  1. 單元測試是可以在本地快速執行的,元件可以通過 mock 生成。其特點是快!
  2. 整合測試只能跑在虛擬機器或者真機,整合了多個系統元件,如無法 mock 的元件—相機呼叫,可用於單個頁面的邏輯正確性測試。其特點是慢!
  3. UI 測試就是模仿使用者真實行為的測試,涉及完整業務流程,這個我們最為熟悉,每次提測 QA 都在手動或者自動做這部分工作。

不同的測試型別,側重面不同,價效比也不同,官方推薦的測試比重是 7:2:1^ref2,也就是說單元測試價效比最高應該佔整個測試的 70%。

單元測試是測試每個方法的正確性,只要保證每個方法都沒有問題,那麼由這些方法組成的模組也不會有太大問題(出問題要麼是邊界沒考慮全要麼是流程有問題),一定程度上起到減少 bug 率的作用,實際專案中已經不記得多少 QA 提的 bug 都是人為疏忽導致得了,通過單元測試都可以輕易避免啊喂。

Bug 少了,人也精神了,簡直不要太爽!

三、我們在寫 Android 單元測試時到底在測什麼?

首先,不知道你是否也有這樣的疑惑:單元測試都是針對純 Java 的,Android 開發和系統元件有著千絲萬縷的聯絡,所以 Android 專案中能寫單元測試的類不多。筆者過去一段時間都是持這種觀點的,大部分 Android 開發同學也不乏這麼想的。

其實不然,藉助第三方庫如 Robolectric 同樣可以像純 Java 類一樣去測試那些依賴系統元件的業務類。

之所以有“依賴系統元件的類不能單元測試”的誤解,官方也有一定的責任,Android Studio 建立的專案預設只有 JUnit4 單元測試示例,要測試系統元件依賴的類,官方的示例是通過 AndroidTest,這是要跑在真機或者虛擬機器上的,不是真正意義上的單元測試。

也就是說 Android 開發中所有的類都可以被單元測試。

其次,我們測試的目的有這幾個方面,統稱為 Right-BICEP^ref3 原則:

  • Right – Are the results right? 結果是否正確?
  • B – are all the boundary conditions correct? 所有邊界條件都是正確的麼?
  • I – can you check the inverse relationships? 能否檢查一下反向關聯?
  • C – can you cross-check results using other means? 能夠使用其他手段交叉檢查一下結果?
  • E – can you force error conditions to happen? 是否可以強制錯誤條件產生?
  • P – are performance characteristics within bounds? 是否滿足效能要求?

###三、Android 單元測試要怎麼做?

要明確一點是,單元測試是一門需要學習的技術,無論單元測試、整合測試還是 UI 測試,他們都分別有自己的技術棧。如果你覺得單元測試需要花很多時間或者無從下手,或許是因為你對這門技術掌握得還不夠多不夠熟練,再者可能專案也沒有很好的測試框架方便我們去寫測試程式碼。

同時先介紹兩個概念:Mock,這是 Android 單元測試最重要的概念,Mock 是指模擬出一個虛擬物件,替換我們原先依賴的真實物件,避免類之間相互影響。另外一個重要概念是 Shadow,是指在 Android SDK 類基礎上封裝一層影子類(如 Activity 和ShadowActivity、TextView 和 ShadowTextView 等),這些影子類,豐富了系統類的行為,提供測試介面。

關於測試理論和技術已經有很多成熟的資料,筆者也寫不出什麼新的花樣,也不是本文的目的所在,這裡不在做過多講述,直接給出結論。

筆者最終選擇的 Android 單元測試技術棧是 JUnit4MokitoRobolectricJaCoCoGitLab

  • JUnit4 是純 Java 單元測試框架,在建立專案時,Android Studio 已經搭建好,我們直接使用即可。
  • Mokito 是用來 Mock 依賴的類或者介面,對那些不容易構建的物件用一個虛擬物件來代替。
  • Robolectric 則在 JVM 中實現了 Android SDK 執行的環境,讓我們無需執行虛擬機器/真機就可以跑單元測試。
  • JaCoCoGitLab 用來搭建單元測試報告平臺,可實現定期執行、自動採集、錯誤報警,提供覆蓋率、通過率資料檢視,後面筆者會專門寫一篇文章進行介紹。

我們來看看 JUnit4MokitoRobolectric 在專案中如何使用,在 module 下的 build.gradle 檔案中進行配置:

// 單元測試 JUnit(由 IDE 自動建立)
testImplementation 'junit:junit:4.12'
// 單元測試 Mokito
testImplementation "org.mockito:mockito-core:2.18.3"
// 單元測試 Robolectric
testImplementation "org.robolectric:robolectric:3.8"
複製程式碼

就這麼幾行程式碼,Android 單元測試基礎環境搭建完成,可以說很簡單了。

我們用簡單的例子說明它們的用法。

假設我們有 Math 類,提供 add(int a, int b) 方法:

待測類

想給 add 方法寫測試用例,Android Studio 中的快捷方式是,對這個類進行右鍵 -> Go to -> Test。

建立測試方法(1)

點選 Create New Test。

建立測試方法(2)

在彈出框中,勾選對應方法,然後點選 OK 按鈕,其他保持不變。

建立測試方法(3)

在目錄選擇彈框中,選擇 ../test/... 目錄,這個才是單元測試的目錄,目錄的選擇決定了是單元測試還是整合測試。然後點選 OK。

建立測試方法(4)

單元測試類/方法就建立好了。

測試類

接下來我們寫測試程式碼,其中 Assert 斷言和 @Test 註解就用到了 JUnit。

測試程式碼

點選測試方法左側的小三角(對方法右鍵也行),選擇 Run xxx,很快就有結果。

執行測試用例

測試用例執行結果

上述例子說明了如何建立測試類/方法,以及簡單的 JUnit 用法,我們來改造一下 Math#add 方法:引數改成 String 型別,在相加之前先呼叫 Validator 物件判斷所傳入的引數是否為數字,而 Validator 通過 init 方法傳進來,目的是為了可測試(有時為了可測試,我們要調整編碼思維,本例中的處理方式只是為了說明問題,並非最優方案,也並非通用方案)。

改造後的 add 方法

這種情況 add 方法依賴了一個外部類 Validator,我們要測試其正確性就要排除依賴的影響,這樣做的目的是錯誤隔離,也就是 Validator 自身的 bug 不會影響到 add 方法,Validator 由其對應的測試用例去保證。

隔離的好處是如果 add 測試用例執行失敗,那麼就能確定問題出在 add 方法中,和 Validator 沒什麼關係。

如何排除依賴類的影響呢?這時候就要請出 Mockito 庫了。我們來看看測試用例怎麼寫。

通過 Mock 隔離依賴

具體直接看註釋,這就是 Mockito 使用的例子。

另外一個例子是,我們有一個 Activity,上面有一個輸入框、一個按鈕、一個 TextView,輸入框輸入人名 Peter, TextView 輸出 Hello, Peter! 截圖如下。

介面示例

因為依賴系統元件,我們用 Robolectric 寫測試用例。

Robolectric 單元測試

同樣請直接看註釋。

以上就是 JUnit、Mockito、Robolectric 組合使用的示例,為了便於理解(引人入坑),舉的例子都比較簡單,實際應用中其實可以玩出很多花樣。

相關文章