Unit Test: ViewModel × LiveData × Coroutines using MockK

邱懷民
一個被音樂耽誤的Android工程師
5 min readMay 25, 2019

寫作緣由

小弟我從 Java 轉 Kotlin 一陣子了,但始終沒有好好認真寫過 Unit Test,最近好不容易下定決心要來寫,卻發現網路上鮮少針對 ViewModel 中使用 Coroutines 和 LiveData 進行測試的文章,於是想來分享一下心得以及遇到的一些問題。

這篇文章會用 MockK 這個 library 實作,如果還沒有用過或是聽過 MockK 的朋友們可以參考強者我學長寫的文章,裡面提到了在 Kotlin 世界裡使用 Mockito 和 MockK 的比較和一些實作範例。

ViewModel 範例

這邊用一個讀取電影的功能作爲範例,ViewModel 的功能為使用 Coroutines 做 Api 的存取,再將拿到的資料存至 LiveData 中。(這邊為了 code sample 的簡潔就暫不考慮 Api 成功與否)

測試前置設定

在寫測試前,想先放上三項需要特別注意的問題,也是小弟我研究比較久的地方,希望能幫助各位朋友們繞開這些坑。

1. Dispatchers.Main( ) 沒辦法在 Unit Test 中使用

這個問題 Kotlin 官方有提供一個 Coroutines testing library,方法為宣告一個 Thread Context 在測試中代替 Dispatchers.Main,在測試結束時 reset 並將 Thread Context 關閉。

// Gradle dependencies
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1'
// Before Test
val mainThreadSurrogate = newSingleThreadContext("UI thread")
Dispatchers.setMain(mainThreadSurrogate)
// After Test
Dispatchers.resetMain()
mainThreadSurrogate.close()

2. 測試有時會出現 Looper is not mocked 錯誤訊息

這問題不是每次執行都會發生,但 Android Studio 官方建議可以在 Gradle 中加入這幾行,避免使用到沒有特別 Mock 的 classes 時報錯。

android {
// ...
testOptions {
unitTests.returnDefaultValues = true
}
}

3. MutableLiveData 為 null

因為 LiveData 本身有可能在非 Main Thread 的地方做操作,所以需要加入 android.arch.core:core-testing 提供的 InstantTaskExecutorRule(),他會幫你 override 掉整個 TaskExecutor,讓所有存取只在 Main Thread 發生,方便我們做測試。

開始測試吧!

MockK 提供了很多測試 Coroutines 的 Functions,你可以用 coEvery 來 mock suspend function 或是用 coVerify 來驗證 suspend function 有沒有被呼叫到。

綜合以上的問題,我們的 ViewModelTest 就大概會長這樣。

總結

整體寫起來還算挺簡潔的,如果要對多個 ViewModel 做測試的話,把 Rule 和 Thread Context 等等的設定做成 BaseUnitTest 會幫你省掉更多時間!

歡迎 Android 同好們交個朋友一起討論,或是以上有什麼不對的地方也可以提出來讓我修正!

如果這篇文章有幫助到你,可以按個Follow,或是拍個手讓我更有動力寫下一篇文章!謝謝大家😍

--

--

邱懷民
一個被音樂耽誤的Android工程師

喜歡抱著吉他彈彈唱唱,夢想是靠著寫程式改變世界。