本程式碼研究室是 Kotlin 進階課程的一部分。只要您按部就班完成程式碼研究室,就能發揮本課程的最大效益。不過,您不一定要這麼做。所有課程程式碼研究室清單均列於進階 Android 版的 Kotlin 程式碼研究室到達網頁中。
引言
實作第一個應用程式時,您可能要先執行程式碼,確認其運作正常。執行了測試,雖然並未執行手動測試。隨著您持續新增及更新功能,您也應該繼續執行程式碼並確認其運作正常。但每次都以手動方式進行,都很容易出現錯別字、容易出錯,而且無法擴充。
電腦在擴充及自動化方面都很不錯!因此,有大型和小型開發人員的開發人員都執行自動測試。這項測試是由軟體所執行,因此您不需要手動操作應用程式來確認程式碼是否正常運作。
本系列程式碼研究室所學的教學內容,旨在為真實的應用程式建立一系列測試 (稱為測試套件)。
第一項程式碼研究室包含 Android 測試功能的基本概念,您將撰寫第一項測試,並瞭解如何測試 LiveData
和 ViewModel
。
須知事項
您應該很熟悉:
- Kotlin 程式設計語言
- 以下核心 Android Jetpack 程式庫如下:
ViewModel
和LiveData
- 應用程式架構 (採用應用程式架構指南和 Android 基礎知識程式碼研究室的模式)
課程內容
您將會瞭解以下主題:
- 如何在 Android 上撰寫及執行單元測試
- 如何使用試駕測試
- 如何選擇檢測設備測試和本機測試
您將會瞭解以下程式庫和程式碼概念:
執行步驟
- 在 Android 中設定、執行及解讀本機和檢測測試。
- 在 Android 上使用 JUnit4 和 Hamcrest 撰寫單元測試。
- 撰寫簡單的
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:探索範例應用程式的程式碼
TO-DO 應用程式以熱門的架構藍圖測試和架構樣本為基礎 (使用範例的被動架構版本)。此應用程式採用應用程式架構指南中的架構。這個模型使用 ViewModel 搭配片段、存放區和聊天室。如果您熟悉下列任一範例,則這個應用程式的架構十分類似:
- 有檢視程式碼研究室的會議室
- Android Kotlin 基礎訓練課程程式碼研究室
- 進階 Android 訓練程式碼研究室
- Android 向日葵範例
- 透過 Kotlin Udacity 訓練課程開發 Android 應用程式
更重要的是,您需瞭解應用程式的一般架構,而不是任何圖層的邏輯理解。
以下是所找到套件的摘要:
套件: | |
| 新增或編輯工作畫面:用於新增或編輯工作的 UI 圖層程式碼。 |
| 資料層:這會處理工作的資料層。當中包含資料庫、網路和存放區程式碼。 |
| 統計資料畫面:統計資料畫面的使用者介面圖層程式碼。 |
| 工作詳細資料畫面:單一工作的 UI 圖層程式碼。 |
| 工作畫面:用於列出所有工作清單的 UI 圖層程式碼。 |
| 公用程式類別:在應用程式各部分的共用類別,例如在多螢幕中使用的滑動重新整理版面配置。 |
資料層 (.data)
這個應用程式包含 remote 套件中的模擬網路層,以及 local 套件中的資料庫層。為求簡單來說,在這個專案中,網路層僅模擬 HashMap
的延遲時間,並且提供延遲和真實的網路要求。
DefaultTasksRepository
會在網路層和資料庫層之間進行協調或調解,並且會將資料傳回 UI 層。
UI 層 ( .addedittask, .statistics, .taskdetail, .tasks)
每個 UI 圖層套件都包含片段和檢視模型,以及使用者介面要求的其他類別 (例如工作清單的轉接程式)。TaskActivity
是包含所有片段的活動。
導覽
應用程式的導覽功能則由導覽元件控管。這是在 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 和 測試) 包含您的測試。根據預設,當您建立新的 Android 專案時,系統會提供以下三個來源集。這些因素包括:
main
:包含您的應用程式程式碼。此程式碼可與您所建立的所有不同應用程式版本共用 (稱為版本變化版本)androidTest
:包含以檢測檢測為基礎的測試。test
:包含以本機測試為基礎的測試。
本機測試和檢測測試之間的差異在於執行的方式。
本機測試 (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
註解開頭的函式 (每個函式都是單一測試)。 - u8sually 包含宣告聲明。
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 提供了各項工具,方便您產生測試用的 stub。
- 在
getActiveAndCompletedStats
上按一下滑鼠右鍵,然後選取 [Generate] (產生) > [測試]。
「Create Test」(建立測試) 對話方塊隨即開啟:
- 將 [Class name:] (類別名稱:) 變更為
StatisticsUtilsTest
(而不是StatisticsUtilsKtTest
;而不是在測試類別名稱中保留 KT)。 - 保留其餘預設值。JUnit 4 是合適的測試程式庫。目的地套件正確無誤 (會反映
StatisticsUtils
類別的位置),您無須勾選任何核取方塊 (這個動作只會產生額外的程式碼,但您會自行撰寫測試)。 - 按一下 [確定]。
系統隨即會開啟「Choose Destination Directory」對話方塊:
由於函式會進行數學計算,因此不會加入任何本機測試,因此不會加入任何 Android 專屬程式碼。因此,您不需要在實際或模擬的裝置上執行該檔案。
- 請選取
test
目錄 (而非androidTest
),因為您會撰寫本機測試。 - 按一下「OK」(確定)。
- 請注意,在
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
上按一下滑鼠右鍵並選取 [執行])。
傳送成功:
步驟 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'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))
}
}
- 執行更新後的測試,確認測試仍然有效!
這個程式碼研究室「不會」教導 Hamcrest 的所有細節,因此如果你想進一步瞭解,請參考官方教學課程。
這是選擇性練習的工作。
在這項工作中,你會使用 JUnit 和 Hamcrest 撰寫更多測試。此外,您採用的測試必須參考「試駕開發」計劃的經營策略。Test Driven Development 或 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()
)
}
若要修正程式碼並撰寫測試,您會使用測試驅動開發。測試試駕步驟如下。
- 請使用「提供了」、「時間」、「時間」結構,以及遵循慣例的名稱來撰寫測試。
- 確認測試失敗。
- 撰寫最少的程式碼,讓測試通過。
- 請重複執行所有測試!
與其先修正錯誤,首先要先撰寫測試。然後,您可以進行測試,避免日後再次發生這些錯誤。
- 如果有空白清單 (
emptyList()
),則兩個百分比都應為 0f。 - 如果載入工作時發生錯誤,清單將顯示為
null
,且兩個百分比均應為 0f。 - 執行測試,並確認測試「失敗」:
步驟 3:修正錯誤
現在您已完成測試,請修正錯誤。
- 如果
tasks
為null
或空白,傳回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
)
}
}
- 再次執行測試,並確認所有測試現已通過!
透過追蹤 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
測試。
在其餘的程式碼研究室中,您將學會如何針對兩種應用程式 (ViewModel
和 LiveData
) 的常見 Android 類別撰寫測試。
請先撰寫TasksViewModel
的測試。
你將會著重在檢視模型中具備所有邏輯的測試,且不仰賴存放區程式碼。存放區的程式碼包含非同步程式碼、資料庫和網路呼叫,全都會增加測試的複雜度。您暫時要避免這個問題,並專注於撰寫「不會」測試存放區中任何事物的 ViewModel 功能。
你撰寫的測試將檢查你是否在呼叫 addNewTask
方法時,會開啟用於開啟新工作視窗的 Event
。這裡是你要測試的應用程式程式碼。
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
步驟 1:建立 TasksViewModelTest 類別
在這個步驟中,您執行的是與 StatisticsUtilTest
相同的步驟,您可以為 TasksViewModelTest
建立測試檔案。
- 開啟您要測試的課程 (在
tasks
套件中,TasksViewModel.
) - 在程式碼中,以滑鼠右鍵按一下課程名稱
TasksViewModel
-> Generate -> Test。
- 在「Create Test」(建立測試) 畫面中,按一下 [OK] (確定) 即可接受變更 (不需變更任何預設設定)。
- 在「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
的執行個體進行測試時,其建構函式需要應用程式內容。不過,在這個測試中,您並未建立內含活動和使用者介面和片段的完整應用程式,因此該如何取得應用程式情況?
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(???)
AndroidX 測試程式庫包含類別和方法,可提供元件版本 (例如用於測試的應用程式和活動) 的測試版本。如果您的本機測試需要模擬 Android 架構類別 (例如應用程式內容),請按照下列步驟正確設定 AndroidX 測試:
- 新增 AndroidX Test 核心和外部依附元件
- 新增 Robolectric Testing Library 依附元件
- 使用 AndroidJunit4 測試執行器為類別加上註解
- 撰寫 AndroidX 測試程式碼
您會完成下列步驟,「之後」就能瞭解兩者如何互相配合。
步驟 3:新增 gradle 依附元件
- 將這些依附元件複製到應用程式的模組
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 測試執行者
- 在測試類別上方新增
@RunWith(AndroidJUnit4::class)
。
TasksViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
步驟 5:使用 AndroidX Test
此時,您可以使用 AndroidX 測試程式庫。這包括 ApplicationProvider.getApplicationContex
t
方法來取得應用程式內容。
- 使用
ApplicationProvider.getApplicationContext()
從 AndroidX 測試程式庫建立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 測試如何運作?
什麼是 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
警告。
- 在 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
,建議您執行以下兩項操作:
- 使用
InstantTaskExecutorRule
- 確保觀察到
LiveData
步驟 1:使用 InstantTaskExecutorRule
InstantTaskExecutorRule
是 JUnit 規則。搭配 @get:Rule
註解使用時,它會讓 InstantTaskExecutorRule
類別中的部分程式碼在測試前後執行 (如要查看確切的程式碼,您可以使用鍵盤快速鍵 Command+B 來查看檔案)。
這項規則會在同一執行緒中執行所有與架構元件相關的背景工作,以同步方式執行測試結果,並以可重複的方式排列。撰寫包含 LiveData 的測試時,請使用這項規則!
- 為架構元件核心測試程式庫 (包含這項規則的規則) 新增 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
方法,這樣一來不必使用 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
」的擴充功能函式,以便輕鬆新增觀察項目。
- 在
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
是否正確。
步驟 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 測試程式庫設定 ViewModel 測試。
Udacity 課程:
Android 開發人員說明文件:
影片:
其他:
如要瞭解本課程中其他程式碼研究室的連結,請參閱 Kotlin 的進階 Android 程式碼研究室到達網頁。