測試基本概念

本程式碼研究室是 Kotlin 進階課程的一部分。只要您按部就班完成程式碼研究室,就能發揮本課程的最大效益。不過,您不一定要這麼做。所有課程程式碼研究室清單均列於進階 Android 版的 Kotlin 程式碼研究室到達網頁中。

引言

實作第一個應用程式時,您可能要先執行程式碼,確認其運作正常。執行了測試,雖然並未執行手動測試。隨著您持續新增及更新功能,您也應該繼續執行程式碼並確認其運作正常。但每次都以手動方式進行,都很容易出現錯別字、容易出錯,而且無法擴充。

電腦在擴充及自動化方面都很不錯!因此,有大型和小型開發人員的開發人員都執行自動測試。這項測試是由軟體所執行,因此您不需要手動操作應用程式來確認程式碼是否正常運作。

本系列程式碼研究室所學的教學內容,旨在為真實的應用程式建立一系列測試 (稱為測試套件)。

第一項程式碼研究室包含 Android 測試功能的基本概念,您將撰寫第一項測試,並瞭解如何測試 LiveDataViewModel

須知事項

您應該很熟悉:

課程內容

您將會瞭解以下主題:

  • 如何在 Android 上撰寫及執行單元測試
  • 如何使用試駕測試
  • 如何選擇檢測設備測試和本機測試

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

執行步驟

  • 在 Android 中設定、執行及解讀本機和檢測測試。
  • 在 Android 上使用 JUnit4 和 Hamcrest 撰寫單元測試。
  • 撰寫簡單的 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:探索範例應用程式的程式碼

TO-DO 應用程式以熱門的架構藍圖測試和架構樣本為基礎 (使用範例的被動架構版本)。此應用程式採用應用程式架構指南中的架構。這個模型使用 ViewModel 搭配片段、存放區和聊天室。如果您熟悉下列任一範例,則這個應用程式的架構十分類似:

更重要的是,您需瞭解應用程式的一般架構,而不是任何圖層的邏輯理解。

以下是所找到套件的摘要:

套件:com.example.android.architecture.blueprints.todoapp

.addedittask

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

.data

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

.statistics

統計資料畫面:統計資料畫面的使用者介面圖層程式碼。

.taskdetail

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

.tasks

工作畫面:用於列出所有工作清單的 UI 圖層程式碼。

.util

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

資料層 (.data)

這個應用程式包含 remote 套件中的模擬網路層,以及 local 套件中的資料庫層。為求簡單來說,在這個專案中,網路層僅模擬 HashMap 的延遲時間,並且提供延遲和真實的網路要求。

DefaultTasksRepository 會在網路層和資料庫層之間進行協調或調解,並且會將資料傳回 UI 層。

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

每個 UI 圖層套件都包含片段和檢視模型,以及使用者介面要求的其他類別 (例如工作清單的轉接程式)。TaskActivity 是包含所有片段的活動。

導覽

應用程式的導覽功能則由導覽元件控管。這是在 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)

這些資料夾稱為來源集。來源集是包含您應用程式原始碼的資料夾。含有綠色色彩的來源集 (androidTest測試) 包含您的測試。根據預設,當您建立新的 Android 專案時,系統會提供以下三個來源集。這些因素包括:

  • 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 註解開頭的函式 (每個函式都是單一測試)。
  • u8sually 包含宣告聲明。

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 提供了各項工具,方便您產生測試用的 stub。

  1. getActiveAndCompletedStats 上按一下滑鼠右鍵,然後選取 [Generate] (產生) > [測試]

「Create Test」(建立測試) 對話方塊隨即開啟:

  1. 將 [Class name:] (類別名稱:) 變更為 StatisticsUtilsTest (而不是 StatisticsUtilsKtTest;而不是在測試類別名稱中保留 KT)。
  2. 保留其餘預設值。JUnit 4 是合適的測試程式庫。目的地套件正確無誤 (會反映 StatisticsUtils 類別的位置),您無須勾選任何核取方塊 (這個動作只會產生額外的程式碼,但您會自行撰寫測試)。
  3. 按一下 [確定]

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

由於函式會進行數學計算,因此不會加入任何本機測試,因此不會加入任何 Android 專屬程式碼。因此,您不需要在實際或模擬的裝置上執行該檔案。

  1. 請選取 test 目錄 (而非 androidTest),因為您會撰寫本機測試。
  2. 按一下「OK」(確定)
  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 上按一下滑鼠右鍵並選取 [執行])。

傳送成功:

步驟 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's (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 撰寫更多測試。此外,您採用的測試必須參考「試駕開發」計劃的經營策略。Test Driven Development 或 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. 請使用「提供了」、「時間」、「時間」結構,以及遵循慣例的名稱來撰寫測試。
  2. 確認測試失敗。
  3. 撰寫最少的程式碼,讓測試通過。
  4. 請重複執行所有測試!

與其先修正錯誤,首先要先撰寫測試。然後,您可以進行測試,避免日後再次發生這些錯誤。

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

步驟 3:修正錯誤

現在您已完成測試,請修正錯誤。

  1. 如果 tasksnull 或空白,傳回 0f 以修正 getActiveAndCompletedStats 中的錯誤:
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 測試。

在其餘的程式碼研究室中,您將學會如何針對兩種應用程式 (ViewModelLiveData) 的常見 Android 類別撰寫測試。

請先撰寫TasksViewModel的測試。


你將會著重在檢視模型中具備所有邏輯的測試,且不仰賴存放區程式碼。存放區的程式碼包含非同步程式碼、資料庫和網路呼叫,全都會增加測試的複雜度。您暫時要避免這個問題,並專注於撰寫「不會」測試存放區中任何事物的 ViewModel 功能。



你撰寫的測試將檢查你是否在呼叫 addNewTask 方法時,會開啟用於開啟新工作視窗的 Event。這裡是你要測試的應用程式程式碼。

TasksViewModel.kt

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

步驟 1:建立 TasksViewModelTest 類別

在這個步驟中,您執行的是與 StatisticsUtilTest 相同的步驟,您可以為 TasksViewModelTest 建立測試檔案。

  1. 開啟您要測試的課程 (在 tasks 套件中,TasksViewModel.)
  2. 在程式碼中,以滑鼠右鍵按一下課程名稱 TasksViewModel -> Generate -> Test

  1. 在「Create Test」(建立測試) 畫面中,按一下 [OK] (確定) 即可接受變更 (不需變更任何預設設定)。
  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 的執行個體進行測試時,其建構函式需要應用程式內容。不過,在這個測試中,您並未建立內含活動和使用者介面和片段的完整應用程式,因此該如何取得應用程式情況?

TasksViewModelTest.kt

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

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

  1. 新增 AndroidX Test 核心和外部依附元件
  2. 新增 Robolectric Testing Library 依附元件
  3. 使用 AndroidJunit4 測試執行器為類別加上註解
  4. 撰寫 AndroidX 測試程式碼

您會完成下列步驟,「之後」就能瞭解兩者如何互相配合。

步驟 3:新增 gradle 依附元件

  1. 將這些依附元件複製到應用程式的模組 build.gradle 檔案,新增 Android Android 測試核心和外部依附元件,以及 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. 使用 ApplicationProvider.getApplicationContext() 從 AndroidX 測試程式庫建立 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 測試如何運作?

什麼是 AndroidX 測試?

AndroidX 測試是一系列測試用程式庫。其中提供類別和方法,提供應用程式和測試活動等元件的版本。舉例來說,您編寫的程式碼就是 AndroidX 測試函式的範例,可用來取得應用程式的內容。

ApplicationProvider.getApplicationContext()

AndroidX Test API 的其中一項優點是,這兩個 API 可同時用於本機測試「與」檢測檢測。這很有幫助,因為:

  • 您可以執行與本機測試或檢測測試相同的測試。
  • 您不需要針對本機和檢測測試測試學習不同的測試 API。

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

  • 如果裝置是檢測設備測試,它會在啟動模擬器或連線至實際裝置時取得實際的應用內容。
  • 如果是本機測試,它會使用模擬的 Android 環境。

什麼是 Robolectric?

AndroidX Test 用於測試本機的 Android 環境是由 Robolectric 提供。Robolectric:一個程式庫可用來建立模擬的 Android 環境,用於執行測試,其執行速度比啟動模擬器或在裝置上執行的速度更快。如果沒有 Robolectric 相依項目,您就會收到以下錯誤訊息:

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

測試執行器是執行測試的 JUnit 元件。如果沒有測試執行器,測試就不會執行。這是由 JUnit 提供的預設測試執行器,可自動接收。@RunWith 取代預設測試執行器。

AndroidJUnit4 測試執行器可讓 AndroidX 測試運作,依其是否使用檢測或本機測試而執行不同的測試。

步驟 6:修正 Robolectric 警告

執行程式碼時,請注意使用 Robolectric。

基於 AndroidX Test 與 AndroidJunit4 測試執行器的緣故,您不用直接撰寫一行 Robolectric 程式碼,就能達到這個目的。

您可能會注意到兩次警告。

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

您可以更新 gradle 檔案來修正 No such manifest file: ./AndroidManifest.xml 警告。

  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。請不要將 Android Studio 設定為使用 Java 9,而是針對這個程式碼研究室,保留您的目標,並將 SDK 編譯為 28。

摘要說明:

  • 純檢視模型測試通常可以在 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. 架構元件核心測試程式庫 (包含這項規則的規則) 新增 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 方法,這樣一來不必使用 LifecycleOwner,系統就能持續監控 LiveData。設定observeForever時,您必須移除觀測器或觀測器洩漏的風險。

如下所示。檢查:

@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 是否正確。

步驟 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 測試程式庫設定 ViewModel 測試。

Udacity 課程:

Android 開發人員說明文件:

影片:

其他:

如要瞭解本課程中其他程式碼研究室的連結,請參閱 Kotlin 的進階 Android 程式碼研究室到達網頁。