這個程式碼研究室是「Android Kotlin 進階功能」課程的一部分。如果您按部就班完成每一堂程式碼研究室課程,就能充分體驗到本課程的價值,但這不是強制要求。如要查看所有課程程式碼研究室,請前往 Android Kotlin 進階功能程式碼研究室登陸頁面。
簡介
實作第一個應用程式的第一項功能時,您可能已執行程式碼,確認功能是否正常運作。您已執行測試,但這是手動測試。隨著您不斷新增及更新功能,您可能也持續執行程式碼並驗證是否正常運作。但每次都手動執行這項操作既費力又容易出錯,而且無法擴大執行。
電腦很擅長擴充和自動化!因此,無論是大型或小型公司的開發人員,都會撰寫自動化測試,這類測試是由軟體執行,不需要手動操作應用程式來驗證程式碼是否正常運作。
在本系列程式碼研究室中,您將學習如何為實際應用程式建立一系列測試 (稱為「測試套件」)。
第一個程式碼研究室會介紹 Android 測試的基本概念,您將編寫第一個測試,並瞭解如何測試 LiveData 和 ViewModel。
必備知識
您必須已經熟悉下列項目:
- Kotlin 程式設計語言
- 下列核心 Android Jetpack 程式庫:
ViewModel和LiveData - 應用程式架構,採用應用程式架構指南和 Android 基本概念程式碼研究室的模式
課程內容
您將瞭解下列主題:
- 如何在 Android 上編寫及執行單元測試
- 如何使用測試導向開發
- 如何選擇檢測設備測試和本機測試
您將瞭解下列程式庫和程式碼概念:
學習內容
- 在 Android 中設定、執行及解讀本機和檢測設備測試。
- 使用 JUnit4 和 Hamcrest 在 Android 中編寫單元測試。
- 編寫簡單的
LiveData和ViewModel測試。
在本系列程式碼研究室中,您將使用 TO-DO Notes 應用程式。這個應用程式可讓您寫下待辦事項,並以清單形式顯示。然後標示為完成或未完成、篩選或刪除。

這個應用程式是以 Kotlin 編寫,有多個畫面、使用 Jetpack 元件,並採用《應用程式架構指南》中的架構。瞭解如何測試這個應用程式後,您就能測試使用相同程式庫和架構的應用程式。
如要開始,請先下載程式碼:
或者,您也可以複製 GitHub 存放區的程式碼:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout starter_code
在這項工作中,您將執行應用程式並探索程式碼集。
步驟 1:執行範例應用程式
下載待辦事項應用程式後,請在 Android Studio 中開啟並執行。應該會編譯。請按照下列步驟探索應用程式:
- 使用加號浮動動作按鈕建立新工作。先輸入標題,然後輸入工作的其他資訊。按一下綠色勾號 FAB 儲存。
- 在工作清單中,按一下剛完成的工作標題,然後查看該工作的詳細資料畫面,即可看到其餘說明。
- 在清單或詳細資料畫面中,勾選該工作的核取方塊,將狀態設為「已完成」。
- 返回工作畫面,開啟篩選選單,然後依「進行中」和「已完成」狀態篩選工作。
- 開啟導覽匣,然後按一下「統計資料」。
- 返回總覽畫面,然後從導覽匣選單中選取「清除已完成」,刪除所有狀態為「已完成」的工作
步驟 2:探索範例應用程式程式碼
「待辦事項」應用程式是以熱門的架構藍圖測試和架構範例為基礎 (使用範例的反應式架構版本)。應用程式採用的是《應用程式架構指南》中的架構。並搭配片段、存放區和 Room 使用 ViewModel。如果您熟悉下列任一範例,這個應用程式的架構與這些範例類似:
- 設有檢視畫面程式碼研究室的 Room
- Android Kotlin 基礎知識訓練程式碼研究室
- 進階 Android 訓練程式碼研究室
- Android Sunflower 範例
- 使用 Kotlin 開發 Android 應用程式 Udacity 訓練課程
您不必深入瞭解任何一層的邏輯,但請務必瞭解應用程式的整體架構。
以下是您會看到的套裝方案摘要:
Package: | |
| 新增或編輯工作畫面:用於新增或編輯工作的 UI 層程式碼。 |
| 資料層:處理工作資料層。其中包含資料庫、網路和存放區程式碼。 |
| 統計資料畫面:統計資料畫面的 UI 層程式碼。 |
| 工作詳細資料畫面:單一工作的 UI 層程式碼。 |
| 工作畫面:所有工作的清單 UI 層代碼。 |
| 公用程式類別:應用程式各部分使用的共用類別,例如用於多個畫面的滑動重新整理版面配置。 |
資料層 (.data)
這個應用程式包含 remote 套件中的模擬網路層,以及 local 套件中的資料庫層。為求簡化,這個專案會使用延遲時間的 HashMap 模擬網路層,而不是發出實際的網路要求。
DefaultTasksRepository 會協調或調解網路層和資料庫層之間的關係,並將資料傳回 UI 層。
UI 層 ( .addedittask、.statistics、.taskdetail、.tasks)
每個 UI 層套件都包含片段和檢視模型,以及 UI 所需的任何其他類別 (例如工作清單的配接器)。TaskActivity 是包含所有片段的活動。
導覽
應用程式的導覽是由 Navigation 元件控制。這是在 nav_graph.xml 檔案中定義的。檢視模型會使用 Event 類別觸發導覽,並決定要傳遞哪些引數。片段會觀察 Event,並在畫面之間執行實際導覽。
在這項工作中,您將執行第一項測試。
- 在 Android Studio 中開啟「Project」窗格,然後找出下列三個資料夾:
com.example.android.architecture.blueprints.todoappcom.example.android.architecture.blueprints.todoapp (androidTest)com.example.android.architecture.blueprints.todoapp (test)
這些資料夾稱為「來源集」。來源集是包含應用程式原始碼的資料夾。來源集 (以綠色標示,即 androidTest 和 test) 包含測試。建立新的 Android 專案時,預設會取得下列三個來源集。這 3 個子類型如下:
本機測試和檢測設備測試的差異在於執行方式。
本機測試 (test 來源集)
這些測試會在開發機的 JVM 上在本機執行,不需要模擬器或實體裝置。因此執行速度很快,但保真度較低,也就是說,動作較不像現實世界中的動作。
在 Android Studio 中,本機測試會以綠色和紅色三角形圖示表示。

檢測設備測試 (androidTest 來源集)
這類測試會在實際或模擬的 Android 裝置上執行,因此能反映現實狀況,但速度也慢得多。
在 Android Studio 中,檢測設備測試會以 Android 裝置表示,並附上綠色和紅色三角形圖示。

步驟 1:執行本機測試
- 開啟
test資料夾,直到找到 ExampleUnitTest.kt 檔案為止。 - 在該檔案上按一下滑鼠右鍵,然後選取「Run ExampleUnitTest」。
畫面底部的「Run」視窗中應會顯示下列輸出內容:

- 請注意綠色勾號,並展開測試結果,確認名為
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。
- 將
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
}
}- 進行測試。
- 在測試結果中,請注意測試旁的 X。

- 另請注意:
- 只要有任何斷言失敗,整個測試就會失敗。
- 系統會顯示預期值 (3) 與實際計算出的值 (2)。
- 系統會將您導向失敗的判斷陳述式
(ExampleUnitTest.kt:16)所在行。
步驟 3:執行檢測設備測試
檢測設備測試位於 androidTest 來源集中。
- 開啟
androidTest來源集。 - 執行名為
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:建立測試類別
- 在
main來源集中,開啟todoapp.statistics中的StatisticsUtils.kt。 - 找出
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 函式會接受工作清單,並傳回 StatsResult。StatsResult 是包含兩個數字的資料類別,分別是已完成的工作百分比,以及進行中的工作百分比。
Android Studio 提供工具,可產生測試存根,協助您實作這項函式的測試。
- 在
getActiveAndCompletedStats上按一下滑鼠右鍵,然後依序選取「Generate」 >「Test」。
|
|
「建立測試」對話方塊隨即開啟:

- 將「類別名稱」變更為
StatisticsUtilsTest(而非StatisticsUtilsKtTest;測試類別名稱中沒有 KT 會比較好)。 - 保留其他預設值。JUnit 4 是適當的測試程式庫。目的地套件正確無誤 (與
StatisticsUtils類別的位置相同),且您不需要勾選任何核取方塊 (這只會產生額外程式碼,但您會從頭編寫測試)。 - 按一下「確定」。
系統會開啟「Choose Destination Directory」對話方塊:
您要進行本機測試,因為函式會執行數學運算,且不會包含任何 Android 專屬程式碼。因此不需要在實體或模擬裝置上執行。
- 選取
test目錄 (而非androidTest),因為您要編寫本機測試。 - 按一下「確定」。
- 請注意,
test/statistics/中已產生StatisticsUtilsTest類別。
步驟 2:編寫第一個測試函式
您要編寫的測試會檢查下列項目:
- 如果沒有已完成的工作,但有一項工作正在進行中,
- 有效測試的百分比為 100%,
- 已完成的工作百分比為 0%。
- 開啟
StatisticsUtilsTest。 - 建立名為
getActiveAndCompletedStats_noCompleted_returnsHundredZero的函式。
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task
// Call your function
// Check the result
}
}- 在函式名稱上方新增
@Test註解,表示這是測試。 - 建立工作清單。
// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)- 使用這些工作呼叫
getActiveAndCompletedStats。
// Call your function
val result = getActiveAndCompletedStats(tasks)- 使用斷言檢查
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)
}
}- 執行測試 (按一下滑鼠右鍵
StatisticsUtilsTest,然後選取「Run」)。
應該會通過:

步驟 3:新增 Hamcrest 依附元件
由於測試可做為程式碼功能的說明文件,因此最好能讓使用者輕鬆閱讀。比較下列兩項斷言:
assertEquals(result.completedTasksPercent, 0f)
// versus
assertThat(result.completedTasksPercent, `is`(0f))第二個判斷陳述式更像人類的句子。這項測試是使用名為 Hamcrest 的斷言架構編寫。Truth 程式庫也是撰寫可讀性高的斷言時,相當實用的工具。在本程式碼研究室中,您將使用 Hamcrest 撰寫斷言。
- 開啟
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 撰寫斷言
- 更新
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))
}
}- 執行更新後的測試,確認測試仍可正常運作!
本程式碼研究室不會教您 Hamcrest 的所有細節,如要進一步瞭解,請參閱官方教學課程。
這是選用練習。
在這項工作中,您將使用 JUnit 和 Hamcrest 撰寫更多測試。您也會使用從「測試驅動開發」的程式實務衍生而來的策略,撰寫測試。測試驅動開發 (TDD) 是一種程式設計思維,主張先編寫測試,再編寫功能程式碼。然後編寫功能程式碼,目標是通過測試。
步驟 1:撰寫測試
如果使用一般工作清單,請編寫下列測試:
- 如果有一項已完成的工作,但沒有進行中的工作,則
activeTasks百分比應為0f,已完成的工作百分比應為100f。 - 如果已完成兩項工作,還有三項工作正在進行中,則完成百分比應為
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()
)
}您將使用測試驅動開發,修正程式碼並編寫測試。測試導向開發遵循下列步驟。
- 使用「Given-When-Then」結構編寫測試,並採用符合慣例的名稱。
- 確認測試失敗。
- 編寫最少的程式碼,讓測試通過。
- 請針對所有測試重複執行上述步驟!

您不會先修正錯誤,而是先撰寫測試。然後確認您有測試,可避免日後不慎重新導入這些錯誤。
- 如果清單為空白 (
emptyList()),則兩個百分比都應為 0f。 - 如果載入工作時發生錯誤,清單會是
null,且兩個百分比都應為 0f。 - 執行測試,確認測試失敗:

步驟 3:修正錯誤
現在您已取得測試,請修正錯誤。
- 修正
getActiveAndCompletedStats中的錯誤,方法是在tasks為null或空白時傳回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
)
}
}- 再次執行測試,確認所有測試現在都已通過!

遵循 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
)
}
}您已掌握撰寫及執行測試的基本概念!接下來,您將瞭解如何編寫基本的 ViewModel 和 LiveData 測試。
在本程式碼研究室的其餘部分,您將瞭解如何為大多數應用程式通用的兩個 Android 類別 (ViewModel 和 LiveData) 編寫測試。
首先,請為 TasksViewModel 編寫測試。
您將著重於測試,這些測試的所有邏輯都在檢視模型中,且不依賴存放區程式碼。存放區程式碼涉及非同步程式碼、資料庫和網路呼叫,這些都會增加測試的複雜度。您現在要避免這種情況,專注於為 ViewModel 功能編寫測試,這些測試不會直接測試存放區中的任何項目。

您撰寫的測試會檢查呼叫 addNewTask 方法時,是否會觸發開啟新工作視窗的 Event。以下是您要測試的應用程式程式碼。
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}步驟 1:建立 TasksViewModelTest 類別
請按照與 StatisticsUtilTest 相同的步驟,在此步驟中為 TasksViewModelTest 建立測試檔案。
- 在
tasks套件中開啟要測試的類別TasksViewModel. - 在程式碼中,對類別名稱
TasksViewModel按一下滑鼠右鍵 ->「產生」 ->「測試」。

- 在「建立測試」畫面上,按一下「確定」接受預設設定 (無須變更任何設定)。
- 在「Choose Destination Directory」對話方塊中,選擇「test」目錄。
步驟 2:開始編寫 ViewModel 測試
在這個步驟中,您要新增檢視區塊模型測試,測試在呼叫 addNewTask 方法時,是否會觸發開啟新工作視窗的 Event。
- 建立名為
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:本機測試
- 新增 AndroidX Test 核心和擴充功能依附元件
- 新增 Robolectric 測試程式庫依附元件
- 使用 AndroidJunit4 測試執行工具為類別加上註解
- 編寫 AndroidX Test 程式碼
您將完成這些步驟,並瞭解這些步驟的共同作用。
步驟 3:新增 Gradle 依附元件
- 將這些依附元件複製到應用程式模組的
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 測試執行器
- 在測試類別上方新增
@RunWith(AndroidJUnit4::class)。
TasksViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}步驟 5:使用 AndroidX Test
現在您可以使用 AndroidX 測試程式庫。包括取得應用程式內容的方法 ApplicationProvider.getApplicationContext。
- 使用 AndroidX 測試程式庫中的
ApplicationProvider.getApplicationContext()建立TasksViewModel。
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())- 使用
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
}- 執行測試,確認運作正常。
概念: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 檔案。
- 在 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,建議您採取下列兩項做法:
- 使用
InstantTaskExecutorRule - 確保
LiveData觀察
步驟 1:使用 InstantTaskExecutorRule
InstantTaskExecutorRule 是 JUnit 規則。搭配 @get:Rule 註解使用時,系統會在測試前後執行 InstantTaskExecutorRule 類別中的部分程式碼 (如要查看確切程式碼,可以使用鍵盤快速鍵 Command+B 查看檔案)。
這項規則會在同一執行緒中執行所有與架構元件相關的背景工作,確保測試結果會同步發生,且順序可重複。編寫包含 LiveData 測試的測試時,請使用這項規則!
- 新增 Architecture Components 核心測試程式庫 (內含這項規則) 的 gradle 依附元件。
app/build.gradle
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"- 開啟「
TasksViewModelTest.kt」 - 在
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,不需要 LifecycleOwner。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 的擴充功能函式,簡化觀察器的新增作業。
- 在
test來源集建立名為LiveDataTestUtil.kt的新 Kotlin 檔案。

- 複製並貼上下方程式碼。
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
}這個方法相當複雜,這個函式會建立名為 getOrAwaitValue 的 Kotlin 擴充功能函式,用於新增觀察器、取得 LiveData 值,然後清除觀察器,基本上就是上述 observeForever 程式碼的簡短可重複使用版本。如要完整瞭解這個類別,請參閱這篇網誌文章。
步驟 3:使用 getOrAwaitValue 撰寫斷言
在這個步驟中,您會使用 getOrAwaitValue 方法,並編寫檢查 newTaskEvent 是否已觸發的判斷提示陳述式。
- 使用
getOrAwaitValue取得newTaskEvent的LiveData值。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()- 確認值不是空值。
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()))
}
}
- 執行程式碼,您會看到測試通過!
現在您已瞭解如何編寫測試,請自行撰寫一個測試。在這個步驟中,請運用所學技能,練習編寫另一個 TasksViewModel 測試。
步驟 1:自行編寫 ViewModel 測試
您將撰寫 setFilterAllTasks_tasksAddViewVisible()。這項測試應會檢查您是否已將篩選器類型設為顯示所有工作,並確認「新增工作」按鈕是否顯示。
- 以
addNewTask_setsNewTaskEvent()做為參考,在TasksViewModelTest中編寫名為setFilterAllTasks_tasksAddViewVisible()的測試,將篩選模式設為ALL_TASKS,並判斷tasksAddViewVisibleLiveData 是否為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. - 新增工作按鈕的顯示狀態由
LiveDatatasksAddViewVisible.控制
- 執行測試。
步驟 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 區塊。
- 建立名為
tasksViewModel|的lateinit執行個體變數。 - 建立名為
setupViewModel的方法。 - 並加上
@Before註解。 - 將檢視區塊模型例項化程式碼移至
setupViewModel。
TasksViewModelTest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}- 執行程式碼!
警告
請「不要」 執行下列操作,也不要初始化
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 中開啟。
本程式碼研究室涵蓋下列內容:
- 如何從 Android Studio 執行測試。
- 本機測試 (
test) 和檢測設備測試 (androidTest) 的差異。 - 如何使用 JUnit 和 Hamcrest 編寫本機單元測試。
- 使用 AndroidX Test Library 設定 ViewModel 測試。
Udacity 課程:
Android 開發人員說明文件:
影片:
其他:
如要查看本課程其他程式碼研究室的連結,請參閱 Android Kotlin 進階功能程式碼研究室登陸頁面。

