測試基本資訊

這個程式碼研究室是「Android Kotlin 進階功能」課程的一部分。如果您按部就班完成每一堂程式碼研究室課程,就能充分體驗到本課程的價值,但這不是強制要求。如要查看所有課程程式碼研究室,請前往 Android Kotlin 進階功能程式碼研究室登陸頁面

簡介

實作第一個應用程式的第一項功能時,您可能已執行程式碼,確認功能是否正常運作。您已執行測試,但這是手動測試。隨著您不斷新增及更新功能,您可能也持續執行程式碼並驗證是否正常運作。但每次都手動執行這項操作既費力又容易出錯,而且無法擴大執行。

電腦很擅長擴充和自動化!因此,無論是大型或小型公司的開發人員,都會撰寫自動化測試,這類測試是由軟體執行,不需要手動操作應用程式來驗證程式碼是否正常運作。

在本系列程式碼研究室中,您將學習如何為實際應用程式建立一系列測試 (稱為「測試套件」)

第一個程式碼研究室會介紹 Android 測試的基本概念,您將編寫第一個測試,並瞭解如何測試 LiveDataViewModel

必備知識

您必須已經熟悉下列項目:

課程內容

您將瞭解下列主題:

  • 如何在 Android 上編寫及執行單元測試
  • 如何使用測試導向開發
  • 如何選擇檢測設備測試和本機測試

您將瞭解下列程式庫和程式碼概念:

學習內容

  • 在 Android 中設定、執行及解讀本機和檢測設備測試。
  • 使用 JUnit4 和 Hamcrest 在 Android 中編寫單元測試。
  • 編寫簡單的 LiveDataViewModel 測試。

在本系列程式碼研究室中,您將使用 TO-DO Notes 應用程式。這個應用程式可讓您寫下待辦事項,並以清單形式顯示。然後標示為完成或未完成、篩選或刪除。

這個應用程式是以 Kotlin 編寫,有多個畫面、使用 Jetpack 元件,並採用《應用程式架構指南》中的架構。瞭解如何測試這個應用程式後,您就能測試使用相同程式庫和架構的應用程式。

如要開始,請先下載程式碼:

下載 ZIP 檔

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout starter_code

在這項工作中,您將執行應用程式並探索程式碼集。

步驟 1:執行範例應用程式

下載待辦事項應用程式後,請在 Android Studio 中開啟並執行。應該會編譯。請按照下列步驟探索應用程式:

  • 使用加號浮動動作按鈕建立新工作。先輸入標題,然後輸入工作的其他資訊。按一下綠色勾號 FAB 儲存。
  • 在工作清單中,按一下剛完成的工作標題,然後查看該工作的詳細資料畫面,即可看到其餘說明。
  • 在清單或詳細資料畫面中,勾選該工作的核取方塊,將狀態設為「已完成」
  • 返回工作畫面,開啟篩選選單,然後依「進行中」和「已完成」狀態篩選工作。
  • 開啟導覽匣,然後按一下「統計資料」
  • 返回總覽畫面,然後從導覽匣選單中選取「清除已完成」,刪除所有狀態為「已完成」的工作

步驟 2:探索範例應用程式程式碼

「待辦事項」應用程式是以熱門的架構藍圖測試和架構範例為基礎 (使用範例的反應式架構版本)。應用程式採用的是《應用程式架構指南》中的架構。並搭配片段、存放區和 Room 使用 ViewModel。如果您熟悉下列任一範例,這個應用程式的架構與這些範例類似:

您不必深入瞭解任何一層的邏輯,但請務必瞭解應用程式的整體架構。

以下是您會看到的套裝方案摘要:

Package: com.example.android.architecture.blueprints.todoapp

.addedittask

新增或編輯工作畫面:用於新增或編輯工作的 UI 層程式碼。

.data

資料層:處理工作資料層。其中包含資料庫、網路和存放區程式碼。

.statistics

統計資料畫面:統計資料畫面的 UI 層程式碼。

.taskdetail

工作詳細資料畫面:單一工作的 UI 層程式碼。

.tasks

工作畫面:所有工作的清單 UI 層代碼。

.util

公用程式類別:應用程式各部分使用的共用類別,例如用於多個畫面的滑動重新整理版面配置。

資料層 (.data)

這個應用程式包含 remote 套件中的模擬網路層,以及 local 套件中的資料庫層。為求簡化,這個專案會使用延遲時間的 HashMap 模擬網路層,而不是發出實際的網路要求。

DefaultTasksRepository 會協調或調解網路層和資料庫層之間的關係,並將資料傳回 UI 層。

UI 層 ( .addedittask、.statistics、.taskdetail、.tasks)

每個 UI 層套件都包含片段和檢視模型,以及 UI 所需的任何其他類別 (例如工作清單的配接器)。TaskActivity 是包含所有片段的活動。

導覽

應用程式的導覽是由 Navigation 元件控制。這是在 nav_graph.xml 檔案中定義的。檢視模型會使用 Event 類別觸發導覽,並決定要傳遞哪些引數。片段會觀察 Event,並在畫面之間執行實際導覽。

在這項工作中,您將執行第一項測試。

  1. 在 Android Studio 中開啟「Project」窗格,然後找出下列三個資料夾:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

這些資料夾稱為「來源集」。來源集是包含應用程式原始碼的資料夾。來源集 (以綠色標示,即 androidTesttest) 包含測試。建立新的 Android 專案時,預設會取得下列三個來源集。這 3 個子類型如下:

  • main:包含應用程式程式碼。您可以建構的所有不同應用程式版本 (稱為「建構變數」) 都會共用這段程式碼
  • androidTest:包含稱為「檢測設備測試」的測試。
  • test:包含稱為本機測試的測試。

本機測試檢測設備測試的差異在於執行方式。

本機測試 (test 來源集)

這些測試會在開發機的 JVM 上在本機執行,不需要模擬器或實體裝置。因此執行速度很快,但保真度較低,也就是說,動作較不像現實世界中的動作。

在 Android Studio 中,本機測試會以綠色和紅色三角形圖示表示。

檢測設備測試 (androidTest 來源集)

這類測試會在實際或模擬的 Android 裝置上執行,因此能反映現實狀況,但速度也慢得多。

在 Android Studio 中,檢測設備測試會以 Android 裝置表示,並附上綠色和紅色三角形圖示。

步驟 1:執行本機測試

  1. 開啟 test 資料夾,直到找到 ExampleUnitTest.kt 檔案為止。
  2. 在該檔案上按一下滑鼠右鍵,然後選取「Run ExampleUnitTest」

畫面底部的「Run」視窗中應會顯示下列輸出內容:

  1. 請注意綠色勾號,並展開測試結果,確認名為 addition_isCorrect 的測試已通過。很高興得知新增功能運作正常!

步驟 2:讓測試失敗

下方是您剛執行的測試。

ExampleUnitTest.kt

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

請注意,測試

  • 是其中一個測試來源集中的類別。
  • 包含以 @Test 註解開頭的函式 (每個函式都是單一測試)。
  • 通常包含判斷提示陳述式。

Android 會使用 JUnit 測試程式庫進行測試 (在本程式碼研究室中為 JUnit4)。判斷和 @Test 註解都來自 JUnit。

判斷述詞是測試的核心。這項程式碼陳述式會檢查程式碼或應用程式是否如預期運作。在本例中,判斷結果為 assertEquals(4, 2 + 2),會檢查 4 是否等於 2 + 2。

如要查看測試失敗的情形,請新增您認為應該會失敗的斷言。這會檢查 3 是否等於 1+1。

  1. assertEquals(3, 1 + 1) 新增至 addition_isCorrect 測試。

ExampleUnitTest.kt

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. 進行測試。
  1. 在測試結果中,請注意測試旁的 X。

  1. 另請注意:
  • 只要有任何斷言失敗,整個測試就會失敗。
  • 系統會顯示預期值 (3) 與實際計算出的值 (2)。
  • 系統會將您導向失敗的判斷陳述式 (ExampleUnitTest.kt:16) 所在行。

步驟 3:執行檢測設備測試

檢測設備測試位於 androidTest 來源集中。

  1. 開啟 androidTest 來源集。
  2. 執行名為 ExampleInstrumentedTest 的測試。

ExampleInstrumentedTest

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

與本機測試不同,這項測試會在裝置上執行 (在下方範例中,是模擬的 Pixel 2 手機):

如果已附加裝置或執行模擬器,您應該會看到測試在模擬器上執行。

在這項工作中,您將為 getActiveAndCompleteStats 編寫測試,這個類別會計算應用程式中已完成和進行中工作統計資料的百分比。您可以在應用程式的統計資料畫面中查看這些數字。

步驟 1:建立測試類別

  1. main 來源集中,開啟 todoapp.statistics 中的 StatisticsUtils.kt
  2. 找出 getActiveAndCompletedStats 函式。

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

getActiveAndCompletedStats 函式會接受工作清單,並傳回 StatsResultStatsResult 是包含兩個數字的資料類別,分別是已完成的工作百分比,以及進行中的工作百分比。

Android Studio 提供工具,可產生測試存根,協助您實作這項函式的測試。

  1. getActiveAndCompletedStats 上按一下滑鼠右鍵,然後依序選取「Generate」 >「Test」

「建立測試」對話方塊隨即開啟:

  1. 將「類別名稱」變更為 StatisticsUtilsTest (而非 StatisticsUtilsKtTest;測試類別名稱中沒有 KT 會比較好)。
  2. 保留其他預設值。JUnit 4 是適當的測試程式庫。目的地套件正確無誤 (與 StatisticsUtils 類別的位置相同),且您不需要勾選任何核取方塊 (這只會產生額外程式碼,但您會從頭編寫測試)。
  3. 按一下「確定」

系統會開啟「Choose Destination Directory」對話方塊:

您要進行本機測試,因為函式會執行數學運算,且不會包含任何 Android 專屬程式碼。因此不需要在實體或模擬裝置上執行。

  1. 選取 test 目錄 (而非 androidTest),因為您要編寫本機測試。
  2. 按一下「確定」。
  3. 請注意,test/statistics/ 中已產生 StatisticsUtilsTest 類別。

步驟 2:編寫第一個測試函式

您要編寫的測試會檢查下列項目:

  • 如果沒有已完成的工作,但有一項工作正在進行中,
  • 有效測試的百分比為 100%,
  • 已完成的工作百分比為 0%。
  1. 開啟 StatisticsUtilsTest
  2. 建立名為 getActiveAndCompletedStats_noCompleted_returnsHundredZero 的函式。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. 在函式名稱上方新增 @Test 註解,表示這是測試。
  2. 建立工作清單。
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. 使用這些工作呼叫 getActiveAndCompletedStats
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. 使用斷言檢查 result 是否符合預期。
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

以下是完整程式碼。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. 執行測試 (按一下滑鼠右鍵 StatisticsUtilsTest,然後選取「Run」)。

應該會通過:

步驟 3:新增 Hamcrest 依附元件

由於測試可做為程式碼功能的說明文件,因此最好能讓使用者輕鬆閱讀。比較下列兩項斷言:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

第二個判斷陳述式更像人類的句子。這項測試是使用名為 Hamcrest 的斷言架構編寫。Truth 程式庫也是撰寫可讀性高的斷言時,相當實用的工具。在本程式碼研究室中,您將使用 Hamcrest 撰寫斷言。

  1. 開啟 build.grade (Module: app) 並新增下列依附元件。

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

新增依附元件時,您通常會使用 implementation,但這裡使用的是 testImplementation。準備好向全球發布應用程式時,最好不要在應用程式中加入任何測試程式碼或依附元件,以免 APK 檔案過大。您可以使用 Gradle 設定,指定程式庫是否應納入主要或測試程式碼。最常見的設定如下:

  • implementation:依附元件適用於所有來源集,包括測試來源集。
  • testImplementation:依附元件僅適用於測試來源集。
  • androidTestImplementation:依附元件僅適用於 androidTest 來源集。

您使用的設定會定義依附元件的使用位置。如果輸入:

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

也就是說,Hamcrest 只能在測試來源集中使用。這也能確保最終應用程式不會包含 Hamcrest。

步驟 4:使用 Hamcrest 撰寫斷言

  1. 更新 getActiveAndCompletedStats_noCompleted_returnsHundredZero() 測試,改用 Hamcrest 的 assertThat,而非 assertEquals
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

請注意,系統可能會提示您使用匯入 import org.hamcrest.Matchers.`is`

最終測試會如下列程式碼所示。

StatisticsUtilsTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. 執行更新後的測試,確認測試仍可正常運作!

本程式碼研究室不會教您 Hamcrest 的所有細節,如要進一步瞭解,請參閱官方教學課程

這是選用練習。

在這項工作中,您將使用 JUnit 和 Hamcrest 撰寫更多測試。您也會使用從「測試驅動開發」的程式實務衍生而來的策略,撰寫測試。測試驅動開發 (TDD) 是一種程式設計思維,主張先編寫測試,再編寫功能程式碼。然後編寫功能程式碼,目標是通過測試。

步驟 1:撰寫測試

如果使用一般工作清單,請編寫下列測試:

  1. 如果有一項已完成的工作,但沒有進行中的工作,則 activeTasks 百分比應為 0f,已完成的工作百分比應為 100f
  2. 如果已完成兩項工作,還有三項工作正在進行中,則完成百分比應為 40f,進行中百分比應為 60f

步驟 2:為錯誤編寫測試

如上所述,getActiveAndCompletedStats 的程式碼有錯誤。請注意,如果清單為空白或空值,這個函式不會正確處理。在這兩種情況下,這兩個百分比都應為零。

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

您將使用測試驅動開發,修正程式碼並編寫測試。測試導向開發遵循下列步驟。

  1. 使用「Given-When-Then」結構編寫測試,並採用符合慣例的名稱。
  2. 確認測試失敗。
  3. 編寫最少的程式碼,讓測試通過。
  4. 請針對所有測試重複執行上述步驟!

您不會先修正錯誤,而是先撰寫測試。然後確認您有測試,可避免日後不慎重新導入這些錯誤。

  1. 如果清單為空白 (emptyList()),則兩個百分比都應為 0f。
  2. 如果載入工作時發生錯誤,清單會是 null,且兩個百分比都應為 0f。
  3. 執行測試,確認測試失敗

步驟 3:修正錯誤

現在您已取得測試,請修正錯誤。

  1. 修正 getActiveAndCompletedStats 中的錯誤,方法是在 tasksnull 或空白時傳回 0f
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. 再次執行測試,確認所有測試現在都已通過!

遵循 TDD 並先編寫測試,有助於確保:

  • 新功能一律會有相關聯的測試,因此測試可做為程式碼功能的說明文件。
  • 測試會檢查結果是否正確,並防範您已發現的錯誤。

解決方案:編寫更多測試

以下是所有測試和對應的功能程式碼。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

您已掌握撰寫及執行測試的基本概念!接下來,您將瞭解如何編寫基本的 ViewModelLiveData 測試。

在本程式碼研究室的其餘部分,您將瞭解如何為大多數應用程式通用的兩個 Android 類別 (ViewModelLiveData) 編寫測試。

首先,請為 TasksViewModel 編寫測試。


您將著重於測試,這些測試的所有邏輯都在檢視模型中,且不依賴存放區程式碼。存放區程式碼涉及非同步程式碼、資料庫和網路呼叫,這些都會增加測試的複雜度。您現在要避免這種情況,專注於為 ViewModel 功能編寫測試,這些測試不會直接測試存放區中的任何項目。



您撰寫的測試會檢查呼叫 addNewTask 方法時,是否會觸發開啟新工作視窗的 Event。以下是您要測試的應用程式程式碼。

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

步驟 1:建立 TasksViewModelTest 類別

請按照與 StatisticsUtilTest 相同的步驟,在此步驟中為 TasksViewModelTest 建立測試檔案。

  1. tasks 套件中開啟要測試的類別 TasksViewModel.
  2. 在程式碼中,對類別名稱 TasksViewModel 按一下滑鼠右鍵 ->「產生」 ->「測試」

  1. 在「建立測試」畫面上,按一下「確定」接受預設設定 (無須變更任何設定)。
  2. 在「Choose Destination Directory」對話方塊中,選擇「test」目錄。

步驟 2:開始編寫 ViewModel 測試

在這個步驟中,您要新增檢視區塊模型測試,測試在呼叫 addNewTask 方法時,是否會觸發開啟新工作視窗的 Event

  1. 建立名為 addNewTask_setsNewTaskEvent 的新測試。

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

應用程式背景資訊呢?

建立 TasksViewModel 例項進行測試時,其建構函式需要 Application Context。但在這項測試中,您不會建立包含活動、UI 和片段的完整應用程式,因此要如何取得應用程式內容?

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

AndroidX Test 程式庫包含類別和方法,可提供適用於測試的元件版本,例如應用程式和活動。如果您需要模擬 Android 架構類別(例如應用程式內容),請按照下列步驟正確設定 AndroidX Test:本機測試

  1. 新增 AndroidX Test 核心和擴充功能依附元件
  2. 新增 Robolectric 測試程式庫依附元件
  3. 使用 AndroidJunit4 測試執行工具為類別加上註解
  4. 編寫 AndroidX Test 程式碼

您將完成這些步驟,並瞭解這些步驟的共同作用。

步驟 3:新增 Gradle 依附元件

  1. 將這些依附元件複製到應用程式模組的 build.gradle 檔案中,即可新增 AndroidX Test 核心和擴充功能依附元件,以及 Robolectric 測試依附元件。

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

步驟 4:新增 JUnit 測試執行器

  1. 在測試類別上方新增 @RunWith(AndroidJUnit4::class)

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

步驟 5:使用 AndroidX Test

現在您可以使用 AndroidX 測試程式庫。包括取得應用程式內容的方法 ApplicationProvider.getApplicationContext

  1. 使用 AndroidX 測試程式庫中的 ApplicationProvider.getApplicationContext() 建立 TasksViewModel

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. 使用 tasksViewModel 撥打電話給addNewTask

TasksViewModelTest.kt

tasksViewModel.addNewTask()

此時,您的測試應如以下程式碼所示。

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. 執行測試,確認運作正常。

概念:AndroidX Test 的運作方式

什麼是 AndroidX Test?

AndroidX Test 是一系列測試程式庫,當中包含類別和方法,可提供適用於測試的元件版本,例如應用程式和活動。舉例來說,您編寫的這段程式碼是 AndroidX Test 函式範例,用於取得應用程式內容。

ApplicationProvider.getApplicationContext()

AndroidX Test API 的優點之一,是可同時用於本機測試檢測設備測試。這項功能很實用,因為:

  • 您可以執行與本機測試或檢測設備測試相同的測試。
  • 您不必為本機測試和檢測設備測試學習不同的測試 API。

舉例來說,由於您是使用 AndroidX Test 程式庫編寫程式碼,因此您可以將 TasksViewModelTest 類別從 test 資料夾移至 androidTest 資料夾,測試仍會執行。getApplicationContext() 的運作方式會因執行的是本機測試還是檢測設備測試而略有不同:

  • 如果是檢測設備測試,系統會在啟動模擬器或連線至實體裝置時,提供實際的應用程式環境。
  • 如果是本機測試,系統會使用模擬的 Android 環境。

什麼是 Robolectric?

AndroidX Test 用於本機測試的模擬 Android 環境是由 Robolectric 提供。Robolectric 程式庫會建立模擬的 Android 測試環境,執行速度比啟動模擬器或在裝置上執行測試更快。如果沒有 Robolectric 依附元件,您會收到這則錯誤訊息:

@RunWith(AndroidJUnit4::class) 的用途為何?

測試執行器 是執行測試的 JUnit 元件。如果沒有測試執行器,測試就無法執行。JUnit 會自動提供預設測試執行器。@RunWith 會替換預設測試執行器。

AndroidJUnit4 測試執行器可讓 AndroidX Test 根據測試是檢測設備測試還是本機測試,以不同方式執行測試。

步驟 6:修正 Robolectric 警告

執行程式碼時,請注意系統使用的是 Robolectric。

由於有 AndroidX Test 和 AndroidJunit4 測試執行工具,您完全不必直接編寫任何 Robolectric 程式碼,就能完成這項作業!

您可能會看到兩則警告。

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

如要修正 No such manifest file: ./AndroidManifest.xml 警告,請更新 Gradle 檔案。

  1. 在 Gradle 檔案中新增下列程式碼,確保使用正確的 Android 資訊清單。您可以使用 includeAndroidResources 選項,在單元測試中存取 Android 資源,包括 AndroidManifest 檔案。

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

「警告」"WARN: Android SDK 29 requires Java 9..."較為複雜,如要在 Android Q 上執行測試,必須使用 Java 9。在本程式碼研究室中,請將目標和編譯 SDK 設為 28,不要嘗試將 Android Studio 設定為使用 Java 9。

摘要:

  • 純檢視模型測試通常可以放在 test 來源集,因為這類程式碼通常不需要 Android。
  • 您可以使用 AndroidX 測試程式庫,取得應用程式和活動等元件的測試版本。
  • 如要在 test 來源集中執行模擬的 Android 程式碼,可以新增 Robolectric 依附元件和 @RunWith(AndroidJUnit4::class) 註解。

恭喜!您已使用 AndroidX 測試程式庫和 Robolectric 執行測試。您的測試尚未完成 (您還沒撰寫判斷陳述式,目前只有 // TODO test LiveData)。您將在下一個步驟中學習如何撰寫判斷陳述式。LiveData

在這項工作中,您將瞭解如何正確地判斷 LiveData 值。

即將從上次中斷的地方繼續播放,但不會顯示模型測試。addNewTask_setsNewTaskEvent

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

如要測試 LiveData,建議您採取下列兩項做法:

  1. 使用 InstantTaskExecutorRule
  2. 確保 LiveData 觀察

步驟 1:使用 InstantTaskExecutorRule

InstantTaskExecutorRuleJUnit 規則。搭配 @get:Rule 註解使用時,系統會在測試前後執行 InstantTaskExecutorRule 類別中的部分程式碼 (如要查看確切程式碼,可以使用鍵盤快速鍵 Command+B 查看檔案)。

這項規則會在同一執行緒中執行所有與架構元件相關的背景工作,確保測試結果會同步發生,且順序可重複。編寫包含 LiveData 測試的測試時,請使用這項規則!

  1. 新增 Architecture Components 核心測試程式庫 (內含這項規則) 的 gradle 依附元件。

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. 開啟「TasksViewModelTest.kt
  2. TasksViewModelTest 類別中新增 InstantTaskExecutorRule

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

步驟 2:新增 LiveDataTestUtil.kt 類別

下一步是確保系統會觀察您測試的 LiveData

使用 LiveData 時,通常會讓活動或片段 (LifecycleOwner) 觀察 LiveData

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

這項觀察結果非常重要。你必須在 LiveData 中有活躍的觀察者,才能

  • 觸發任何 onChanged 事件。
  • 觸發任何轉換

如要取得檢視畫面模型 LiveData 的預期 LiveData 行為,您需要使用 LifecycleOwner 觀察 LiveData

這會造成問題:在 TasksViewModel 測試中,您沒有活動或片段可觀察 LiveData。如要解決這個問題,可以使用 observeForever 方法,確保系統持續觀察 LiveData,不需要 LifecycleOwnerobserveForever 時,請務必移除觀察器,否則可能會發生觀察器洩漏問題。

程式碼如下所示。檢查:

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

在測試中觀察單一 LiveData 需要大量樣板程式碼!有幾種方法可以移除這段樣板文字。您將建立名為 LiveDataTestUtil 的擴充功能函式,簡化觀察器的新增作業。

  1. test 來源集建立名為 LiveDataTestUtil.kt 的新 Kotlin 檔案。


  1. 複製並貼上下方程式碼。

LiveDataTestUtil.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

這個方法相當複雜,這個函式會建立名為 getOrAwaitValueKotlin 擴充功能函式,用於新增觀察器、取得 LiveData 值,然後清除觀察器,基本上就是上述 observeForever 程式碼的簡短可重複使用版本。如要完整瞭解這個類別,請參閱這篇網誌文章

步驟 3:使用 getOrAwaitValue 撰寫斷言

在這個步驟中,您會使用 getOrAwaitValue 方法,並編寫檢查 newTaskEvent 是否已觸發的判斷提示陳述式。

  1. 使用 getOrAwaitValue 取得 newTaskEventLiveData 值。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. 確認值不是空值。
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

完整的測試應如下所示。

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. 執行程式碼,您會看到測試通過!

現在您已瞭解如何編寫測試,請自行撰寫一個測試。在這個步驟中,請運用所學技能,練習編寫另一個 TasksViewModel 測試。

步驟 1:自行編寫 ViewModel 測試

您將撰寫 setFilterAllTasks_tasksAddViewVisible()。這項測試應會檢查您是否已將篩選器類型設為顯示所有工作,並確認「新增工作」按鈕是否顯示。

  1. addNewTask_setsNewTaskEvent() 做為參考,在 TasksViewModelTest 中編寫名為 setFilterAllTasks_tasksAddViewVisible() 的測試,將篩選模式設為 ALL_TASKS,並判斷 tasksAddViewVisible LiveData 是否為 true


使用下方程式碼即可開始。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

注意:

  • 所有工作的 TasksFilterType 列舉為 ALL_TASKS.
  • 新增工作按鈕的顯示狀態由 LiveData tasksAddViewVisible. 控制
  1. 執行測試。

步驟 2:將測試結果與解決方案比較

請將您的解決方案與下方的解決方案進行比較。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

請確認你是否執行下列操作:

  • 您可以使用相同的 AndroidX ApplicationProvider.getApplicationContext() 陳述式建立 tasksViewModel
  • 呼叫 setFiltering 方法,並傳入 ALL_TASKS 篩選器型別列舉。
  • 您可以使用 getOrAwaitNextValue 方法檢查 tasksAddViewVisible 是否為 true。

步驟 3:新增 @Before 規則

請注意,在兩項測試開始時,您都會定義 TasksViewModel

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

如果多項測試都有重複的設定程式碼,您可以使用 @Before 註解建立設定方法,並移除重複的程式碼。由於所有這些測試都要測試 TasksViewModel,且需要檢視模型,因此請將這段程式碼移至 @Before 區塊。

  1. 建立名為 tasksViewModel|lateinit 執行個體變數。
  2. 建立名為 setupViewModel 的方法。
  3. 並加上 @Before 註解。
  4. 將檢視區塊模型例項化程式碼移至 setupViewModel

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. 執行程式碼!

警告

請「不要」 執行下列操作,也不要初始化

tasksViewModel

及其定義:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

這樣一來,所有測試都會使用同一個執行個體。您應避免這種做法,因為每個測試都應有受測主體 (本例中的 ViewModel) 的全新執行個體。

TasksViewModelTest 的最終程式碼應如下所示。

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

按一下這裡,即可查看您開始時的程式碼與最終程式碼之間的差異。

完成程式碼研究室後,如要下載當中用到的程式碼,可以使用以下 Git 指令:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1


您也可以將存放區下載為 ZIP 檔案、將其解壓縮,並在 Android Studio 中開啟。

下載 ZIP 檔

本程式碼研究室涵蓋下列內容:

  • 如何從 Android Studio 執行測試。
  • 本機測試 (test) 和檢測設備測試 (androidTest) 的差異。
  • 如何使用 JUnitHamcrest 編寫本機單元測試。
  • 使用 AndroidX Test Library 設定 ViewModel 測試。

Udacity 課程:

Android 開發人員說明文件:

影片:

其他:

如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 進階功能程式碼研究室登陸頁面。