這個程式碼研究室是「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.todoapp
com.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.getApplicationContex
t
。
- 使用 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
,並判斷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.
控制
- 執行測試。
步驟 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 進階功能程式碼研究室登陸頁面。