iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Mobile Development

Jetpack Compose 從心開始系列 第 13

Jetpack Compose 從心開始 Day13 - ViewModel 的單元測試

  • 分享至 

  • xImage
  •  

前言

    撰寫 ViewModel 的單元測試是確保 Android 應用程式品質的重要一環。透過測試,可以及早發現並修復問題,提高應用程式的穩定性。

為什麼要測試 ViewModel?

  • 確保邏輯正確性: ViewModel 負責處理應用程式的業務邏輯,測試可以確保這些邏輯運作正確。
  • 提早發現問題: 在開發過程中及早發現並修復 bug,減少後期維護成本。
  • 提高程式碼品質: 測試驅動開發 (TDD) 可以幫助寫出更乾淨、更可維護的程式碼。

新增測試依附元件

在 app 的 build.gradle.kts

dependencies {

    ...
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
    
    testImplementation("junit:junit:4.13.2")
    
    // Import the Compose BOM
    implementation(platform("androidx.compose:compose-bom:2023.06.01"))
    implementation("androidx.compose.material:material")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")

    // ...
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}

建立測試

良好的單元測試通常具備下列四種屬性:

  • 聚焦:將焦點放在測試單元,例如程式碼片段。這段程式碼通常是類別或方法。測試應聚焦於較小的程式碼,且應著重驗證個別程式碼的正確性,而非同時執行多段程式碼。
  • 清楚易懂:程式碼應讀起來簡單易懂。開發人員一眼就能瞭解測試背後的意圖。
  • 確定性:應一律通過或失敗。無論執行測試幾次,只要沒有對程式碼進行任何變更,測試應該都會產生相同的結果。測試不應凌亂,在某個執行個體中失敗,卻在另一個執行個體中成功,儘管沒有修改程式碼。
  • 獨立模式:不需要人為操作或設定,就能獨立執行。

在專案的 路徑(test) 新增測試程式
https://ithelp.ithome.com.tw/upload/images/20240923/20121643AhFufOyYJC.png

錯誤路徑單元測試

@Test
    fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
        // Given an incorrect word as input
        val incorrectPlayerWord = "and"

        viewModel.updateUserGuess(incorrectPlayerWord)
        viewModel.checkUserGuess()

        val currentGameUiState = viewModel.uiState.value
        // Assert that score is unchanged
        assertEquals(0, currentGameUiState.score)
        // Assert that checkUserGuess() method updates isGuessedWordWrong correctly
        assertTrue(currentGameUiState.isGuessedWordWrong)
    }

測試 UI 的初始狀態

@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

    // Assert that current word is scrambled.
    assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
    // Assert that current word count is set to 1.
    assertTrue(gameUiState.currentWordCount == 1)
    // Assert that initially the score is 0.
    assertTrue(gameUiState.score == 0)
    // Assert that the wrong word guessed is false.
    assertFalse(gameUiState.isGuessedWordWrong)
    // Assert that game is not over.
    assertFalse(gameUiState.isGameOver)
}

猜出所有字詞後測試 UI 狀態-正確

@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {
        expectedScore += SCORE_INCREASE
        viewModel.updateUserGuess(correctPlayerWord)
        viewModel.checkUserGuess()
        currentGameUiState = viewModel.uiState.value
        correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
        // Assert that after each correct answer, score is updated correctly.
        assertEquals(expectedScore, currentGameUiState.score)
    }
    // Assert that after all questions are answered, the current word count is up-to-date.
    assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
    // Assert that after 10 questions are answered, the game is over.
    assertTrue(currentGameUiState.isGameOver)
}

測試執行個體生命週期總覽

@Test
    fun gameViewModel_Initialization_FirstWordLoaded() {
        
        val gameUiState = viewModel.uiState.value
        val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

        // Assert that current word is scrambled.
        assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
        // Assert that current word count is set to 1.
        assertTrue(gameUiState.currentWordCount == 1)
        // Assert that initially the score is 0.
        assertTrue(gameUiState.score == 0)
        // Assert that wrong word guessed is false.
        assertFalse(gameUiState.isGuessedWordWrong)
        // Assert that game is not over.
        assertFalse(gameUiState.isGameOver)
    }

程式碼涵蓋率簡介

程式碼涵蓋率扮演著重要角色,會決定您必須對應用程式組成類別、方法和程式碼是否充分測試。
Android Studio 提供了本機單元測試工具的測試涵蓋範圍工具,可用於追蹤單元測試所涵蓋應用程式程式碼的百分比和範圍。

在「Project」窗格中的 GameViewModelTest.kt 檔案上按一下滑鼠右鍵,然後選取「Run 'GameViewModelTest' with Coverage」。
https://ithelp.ithome.com.tw/upload/images/20240923/20121643Wqp3K1aWW1.png

在「程式」窗格中的 class GameViewModelTest 左邊按一下滑鼠右鍵,然後選取「Run 'GameViewModelTest' with Coverage」。
https://ithelp.ithome.com.tw/upload/images/20240923/20121643RcOExY41U8.png

就會有結果 GameViewModel 元素,確認涵蓋率百分比為 100%。最終涵蓋率報表

https://ithelp.ithome.com.tw/upload/images/20240923/20121643PikCHAehpO.png

參考

https://developer.android.com/codelabs/basic-android-kotlin-compose-test-viewmodel


上一篇
Jetpack Compose 從心開始 Day12 - 單向資料流
下一篇
Jetpack Compose 從心開始 Day14 - Navigation 導覽
系列文
Jetpack Compose 從心開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言