Android Unit Test單元測試
單元測試 (Unit Testing) 是針對應用程式中最小可測試單元(通常是函式或類別)進行的驗證。在 Android 中,穩定的單元測試能大幅縮短開發週期,並確保核心邏輯在重構過程中不被破壞。
測試結構:AAA 模式
一個編排良好的測試案例應遵循 AAA 模式,這能讓測試代碼極具可讀性:
- Arrange (準備):初始化物件、Mock 依賴項、準備輸入數據。
- Act (執行):呼叫被測試的方法。
- Assert (斷言):驗證結果是否符合預期。
@Test
fun `測試加法器應回傳正確總和`() {
// Arrange
val calculator = Calculator()
// Act
val result = calculator.add(2, 3)
// Assert (使用 Google Truth 斷言庫)
assertThat(result).isEqualTo(5)
}
使用反引號 (Backtick) 的特殊用途
在 Kotlin 中,被反引號 (`) 包圍的字串可以作為合法的標識符(如函式名、變數名)。這在單元測試中有兩個非常實用的用途:
1. 編寫具備描述性的測試名稱
在 Java 或其他語言中,你可能需要使用駝峰式 (testUserLoginSuccess) 或底線式 (test_user_login_success) 來命名測試,但這往往不夠直觀。Kotlin 允許你在測試名稱中使用空格:
@Test
fun `當使用者輸入正確密碼時,應成功登入並跳轉首頁`() {
// 這種寫法讓測試報告看起來像是一份規格文件
}
2. 解決關鍵字衝突 (Keyword Escaping)
當你需要呼叫 Java 程式碼中與 Kotlin 關鍵字重名的方法時,反引號是唯一的解決方案。例如,Java 類別中有一個方法叫做 is():
// 無法直接呼叫,因為 is 是 Kotlin 的關鍵字
// mock.`is`() ✅ 使用反引號逃逸關鍵字
進階 Mock 技巧 (Mockk)
當測試對象(如 ViewModel)依賴於外部資源(如 Repository)時,我們使用 Mockk 來模擬這些行為。
基本 Stubbing 與 coEvery
對於解除協程掛起函式的 Mock,需使用 coEvery。
coEvery { repository.getUsers() } returns listOf(User("Mike"))
使用 Slots 捕獲參數
當你想驗證傳入 Mock 物件的參數內容時,可以使用 slot。
val slot = slot<User>()
coEvery { repository.saveUser(capture(slot)) } just runs
viewModel.register("Mike")
// 驗證傳給 repository 的 User 物件名稱是否正確
assertThat(slot.captured.name).isEqualTo("Mike")
Answers:動態回傳結果
如果回傳值需要根據傳入參數變動,可以使用 answers。
every { api.fetch(any()) } answers {
val id = firstArg<String>()
if (id == "admin") "Admin Data" else "User Data"
}
協程測試 (Testing Coroutines)
要在單元測試中處理協程,必須解決 Main 執行緒在 JVM 環境下缺失的問題。
MainDispatcherRule
建立一個 JUnit Rule 來自動切換測試用的 Dispatcher。
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
使用 runTest
runTest 能跳過协程中的延遲(如 delay()),讓測試瞬間完成。
@get:Rule val mainRule = MainDispatcherRule()
@Test
fun `測試背景同步邏輯`() = runTest {
viewModel.startSync()
advanceUntilIdle() // 強制執行完所有掛起任務
assertThat(viewModel.isComplete.value).isTrue()
}
Flow 測試與 Turbine
測試 Kotlin Flow 時,建議使用 Turbine 函式庫,它能讓你像處理列表一樣預期 Flow 發出的值。
@Test
fun `測試狀態流變動`() = runTest {
viewModel.uiState.test {
// 初始狀態應為 Loading
assertThat(awaitItem()).isInstanceOf(UiState.Loading::class.java)
viewModel.loadData()
// 隨後應發出 Success
assertThat(awaitItem()).isInstanceOf(UiState.Success::class.java)
// 確保沒有更多事件
cancelAndIgnoreRemainingEvents()
}
}
狀態驗證 vs 互動驗證
- 狀態驗證 (State Testing):檢查物件的屬性或回傳值(如驗證
uiState)。這是最推薦的做法,因為它不依賴實作細節。 - 互動驗證 (Interaction Testing):驗證某個函式是否有被呼叫(使用
verify)。
// 互動驗證:確保 repository.getUsers() 真標被呼叫過一次
coVerify(exactly = 1) { repository.getUsers() }
準則: 優先驗證「結果」(狀態),只有在沒有狀態可查(如呼叫分析工具上報)時,才驗證「互動」。
透過系統化的單元測試,你的 Android 程式碼將變得更加強健且易於長期維護。