測試 Cupcake 應用程式

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 簡介

使用 Compose 瀏覽畫面程式碼研究室中,您已瞭解如何使用 Jetpack Navigation Compose 元件將導覽功能新增至 Compose 應用程式。

Cupcake 應用程式有多個畫面可瀏覽,且可讓使用者執行各種動作。這個應用程式提供了絕佳的機會,可幫助您強化自動化測試技巧!在本程式碼研究室中,您將為 Cupcake 應用程式編寫一些 UI 測試,並瞭解如何盡量提高測試涵蓋範圍。

必要條件

課程內容

  • 使用 Compose 測試 Jetpack 導覽元件。
  • 為每個 UI 測試建立一致的 UI 狀態。
  • 建立測試的輔助函式。

建構項目

  • Cupcake 應用程式的 UI 測試

軟硬體需求

  • 最新版 Android Studio
  • 可下載範例程式碼的網際網路連線

2. 下載範例程式碼

  1. 在 Android Studio 中開啟 basic-android-kotlin-compose-training-cupcake 資料夾。
  2. 在 Android Studio 中開啟 Cupcake 應用程式程式碼。

3. 設定 Cupcake 以進行 UI 測試

新增 androidTest 依附元件

Gradle 建構工具可讓您新增特定模組的依附元件。這項功能可以避免不必要的編譯。在專案中納入依附元件時,您已熟悉 implementation 設定。你已使用這個關鍵字在應用程式模組的 build.gradle 檔案中匯入依附元件。使用 implementation 關鍵字,即可在該模組中的所有來源集中使用依附元件。到目前為止,您已經接觸過 maintestandroidTest 來源集。

UI 測試包含在名為 androidTest 的專屬來源集中。只需要這個模組所需的依附元件,就不必針對其他模組編譯,例如包含應用程式程式碼的 main 模組。新增僅供 UI 測試使用的依附元件時,請使用 androidTestImplementation 關鍵字,在應用程式模組的 build.gradle 檔案中宣告依附元件。如此一來,只有在您執行 UI 測試時,才會編譯UI測試依附元件。

請完成下列步驟,新增寫入 UI 測試所需的依附元件:

  1. 開啟 app/build.gradle 檔案。
  2. 在檔案的依附元件區段中新增下列依附元件:
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation "androidx.navigation:navigation-testing:2.5.0"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"

建立 UI 測試目錄

  1. 在專案檢視畫面中的 src 目錄上按一下滑鼠右鍵,然後依序點選新增 >目錄

288ebc5eae5fba2e.png

  1. 選取androidTest/java選項。

12ec57f9d9e907de.png

建立測試套件

  1. 在專案視窗中的 androidTest/java 目錄上按一下滑鼠右鍵,然後依序點選新增 >套件

5104da51d0529eb7.png

  1. 將套件命名為 com.example.cupcake.testda0e0458fdcb1cfc.png

建立導覽測試類別

test 目錄中,建立名為 CupcakeScreenNavigationTest 的新 Kotlin 類別。

7336dbed4e215b68.png

4. 設定導覽主機

在先前的程式碼研究室中,您瞭解 Compose 中的 UI 測試需要 Compose 測試規則。測試 Jetpack Navigation 也是如此。不過,測試導覽時必須進行一些撰寫撰寫規則的額外設定。

測試 Compose 導覽時,您無法存取在應用程式程式碼中相同的 NavHostController。不過,您可以使用 TestNavHostController 並設定這個導覽控制器的測試規則。本節將說明如何設定並重複使用導覽規則來進行導覽測試。

  1. CupcakeScreenNavigationTest.kt 中,使用 createAndroidComposeRule 建立測試規則,並將 ComponentActivity 做為類型參數傳遞。
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

為確保應用程式導覽至正確的位置,您必須在應用程式執行操作導覽時,參照 TestNavHostController 執行個體,檢查導覽主機的導覽路徑。

  1. TestNavHostController 執行個體執行個體化為 lateinit 變數。在 Kotlin 中,lateinit 關鍵字會宣告可在宣告物件後初始化的屬性。
private lateinit var navController: TestNavHostController

接著,指定要用於 UI 測試的可組合元件。

  1. 建立名為 setupCupcakeNavHost() 的方法。
  2. setupCupcakeNavHost() 方法中,呼叫您建立的 Compose 測試規則中的 setContent() 方法。
  3. 在傳遞至 setContent() 方法的 lambda 中,呼叫 CupcakeApp() 可組合元件。
fun setupCupcakeNavHost() {
   composeTestRule.setContent {
       CupcakeApp()
   }
}

現在,您必須在測試類別中建立 TestNavHostContoller 物件。應用程式稍後會使用控制器來瀏覽 Cupcake 應用程式中的各種畫面,您稍後可以使用這個物件來決定導覽狀態。

  1. 使用您先前建立的 lambda 設定導覽主機。初始化您建立的 navController 變數、註冊導覽器,然後將該 TestNavHostController 傳遞至 CupcakeApp 可組合元件。
fun setupCupcakeNavHost() {
   composeTestRule.setContent {
       navController =
           TestNavHostController(LocalContext.current)
       navController.navigatorProvider.addNavigator(
           ComposeNavigator()
       )
       CupcakeApp(navController = navController)
   }
}

CupcakeScreenNavigationTest 類別中的每個測試都涉及測試導覽的一方面。因此,每項測試都會取決於您建立的 TestNavHostController 物件。您不必在每次測試時手動呼叫 setupCupcakeNavHost() 函式來設定導覽控制器,您可以使用 junit 程式庫提供的 @Before 註解來自動執行這項作業。使用 @Before 註解方法時,先在每個方法加上 @Test 註解之前執行。

  1. 將 @Test 註解新增至 setupCupcakeNavHost() 方法。
@Before
fun setupCupcakeNavHost() {
   composeTestRule.setContent {
       navController =
           TestNavHostController(LocalContext.current)
       navController.navigatorProvider.addNavigator(
           ComposeNavigator()
       )
       CupcakeApp(navController = navController)
   }
}

5. 編寫導覽測試

驗證起始目的地

提醒您,當您在建構 Cupcake 應用程式時,您建立了名為 Cupcakeenum 類別,其中包含常數來控管應用程式的導覽功能。

CupcakeScreen.kt

/**
* enum values that represent the screens in the app
*/
enum class CupcakeScreen(@StringRes val title: Int) {
   Start(title = R.string.app_name),
   Flavor(title = R.string.choose_flavor),
   Pickup(title = R.string.choose_pickup_date),
   Summary(title = R.string.order_summary)
}

所有擁有 UI 的應用程式都有一些主畫面。如果是 Cupcake,這個畫面是開始訂購螢幕CupcakeApp 可組合元件中的導覽控制器會使用 CupcakeScreen 列舉的 Start 項目來決定何時前往該螢幕。應用程式啟動後,如果目的地路徑不存在,導覽主機目的地路徑會設為 Cupcake.Start.name

您必須先撰寫測試,確認應用程式啟動時,開始訂購螢幕是目前的目的地路徑。

  1. 建立名為 cupcakeNavHost_verifyStartDestination() 的函式,並以 @Test 加上註解。
@Test
fun cupcakeNavHost_verifyStartDestination() {
}

現在,您必須確認導覽控制器的初始目的地路徑是「開始訂購螢幕」。

  1. 斷言的預期路徑名稱 (本例中為 Cupcake.Start.name) 等於導覽控制器目前返回堆疊項目的目的地路徑。
import org.junit.Assert.assertEquals
...

@Test
fun cupcakeNavHost_verifyStartDestination() {assertEquals(Cupcake.Start.name, currentBackStackEntry?.destination?.route)
}

建立 Helper 方法

UI 測試通常需要重複步驟,才能將 UI 的特定部分設為可測試特定部分的 UI。自訂 UI 可能也會需要需要多行程式碼的複雜斷言。您在上一節撰寫的斷言需要大量的程式碼,而且您在 Cupcake 應用程式中測試導覽時多次使用同一宣告。在這些情況下,在測試中編寫 helper 方法,即可避免編寫重複的程式碼。

針對每個您撰寫的導覽測試,您可以使用 CupcakeScreen 列舉項目的 name 屬性,檢查導覽控制器目前的目的地路徑是否正確。您必須編寫 helper 函式,以便隨時斷言。

如要建立輔助函式,請完成下列步驟:

  1. test 目錄中建立名為 ScreenAssertions 的空白 Kotlin 檔案。

ac62e5b9b8153027.png

  1. 將擴充功能函式新增至名為 assertCurrentRouteName()NavController 類別,並在方法簽名中傳遞預期路徑名稱的字串。
fun NavController.assertCurrentRouteName(expectedRouteName: String) {

}
  1. 在這個函式中,斷言 expectedRouteName 等於導覽控制器目前的返回堆疊項目目的地路徑。
import org.junit.Assert.assertEquals
...

fun NavController.assertCurrentRouteName(expectedRouteName: String) {
   assertEquals(expectedRouteName, currentBackStackEntry?.destination?.route)
}
  1. 開啟 CupcakeScreenNavigationTest 檔案,並修改 cupcakeNavHost_verifyStartDestination() 函式,以使用新的擴充功能函式,而非冗長的宣告。
@Test
fun cupcakeNavHost_verifyStartDestination() {
   navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

確認「開始」畫面沒有「向上」按鈕

Cupcake 應用程式的原始設計中,「開始」螢幕的工具列沒有「向上」按鈕。

e6d3d87788ba56c8.png

「開始」畫面缺少一個按鈕,因為這個畫面是初始畫面,所以無法向上導覽。請按照下列步驟建立函式,確認「開始」畫面沒有「向上」按鈕:

  1. 建立名為 cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() 的方法,並以 @Test 加上註解。
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
}

在 Cupcake 中,「向上」按鈕的內容說明會設為 R.string.back_button 資源的字串。

  1. 在測試函式中使用 R.string.back_button 資源的值建立變數。
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
   val backText = composeTestRule.activity.getString(R.string.back_button)
}
  1. 斷言中,螢幕上不存在包含此內容說明的節點。
@Test
fun cupcakeNavHost_verifyBackNavigationNotShownOnStartOrderScreen() {
   val backText = composeTestRule.activity.getString(R.string.back_button)
   composeTestRule.onNodeWithContentDescription(backText).assertDoesNotExist()
}

驗證前往口味螢幕

按一下「開始」畫面上的其中一個按鈕,會觸發一個方法,指示導覽控制器前往口味螢幕。

fb8f61896bfa473c.png

在本測試中,您將編寫指令來點選按鈕以觸發此導覽,並驗證目的地路徑是口味螢幕。

  1. 建立名為 cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() 的函式,並以 @Test 加上註解。
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen(){
}
  1. 依據字串資源 ID 尋找一個紙杯蛋糕按鈕,然後對其執行點擊動作。
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() {
   composeTestRule.onNodeWithStringId(R.string.one_cupcake)
       .performClick()
}
  1. 斷言目前的路徑名稱為口味螢幕名稱。
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() {
   composeTestRule.onNodeWithStringId(R.string.one_cupcake)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Flavor.name)
}

編寫更多 helper 方法

Cupcake 應用程式主要具有線性導覽流程。短按取消按鈕後,就只能朝一個方向瀏覽應用程式。因此,測試應用程式中的深度畫面時,您可以發現自己重複執行程式碼,前往要測試的區域。在這種情況下,則會使用更多 helper 方法,因此您只需要編寫一次程式碼。

現在,您已測試前往口味螢幕的導覽功能,請建立可前往口味螢幕的方法,這樣就不需要在日後重複測試該程式碼。

  1. 建立名為 navigateToFlavorScreen() 的方法。
private fun navigateToFlavorScreen() {
}
  1. 編寫指令,找出一個紙杯蛋糕按鈕,並執行上一個部分的點擊動作,就像您在上一節一樣。
private fun navigateToFlavorScreen() {
   composeTestRule.onNodeWithStringId(R.string.one_cupcake)
       .performClick()
}

提醒您,在選取口味之前,口味螢幕中的下一步按鈕不可被點擊。這個方法只能用來準備導航。呼叫此方法後,UI 應處於下一步按鈕可點擊的狀態。

  1. 在 UI 中找到具有 R.string.chocolate 字串的節點,然後對其執行點擊動作來選取節點。
private fun navigateToFlavorScreen() {
   composeTestRule.onNodeWithStringId(R.string.one_cupcake)
       .performClick()
   composeTestRule.onNodeWithStringId(R.string.chocolate)
       .performClick()
}

請確認您是否能編寫 helper 方法,以前往「取貨」畫面和「摘要」畫面。請自行嘗試使用此練習,再查看解決方案。

private fun navigateToPickupScreen() {
   navigateToFlavorScreen()
   composeTestRule.onNodeWithStringId(R.string.next)
       .performClick()
}

private fun navigateToSummaryScreen() {
   navigateToPickupScreen()
   composeTestRule.onNodeWithText(getFormattedDate())
       .performClick()
   composeTestRule.onNodeWithStringId(R.string.next)
       .performClick()
}

當您測試「開始」畫面以外的畫面時,必須規劃「向上」按鈕功能,確保畫面會導向上一個畫面。請考慮建立 helper 函式,找出「向上」按鈕,然後按一下。

private fun performNavigateUp() {
   val backText = composeTestRule.activity.getString(R.string.back_button)
   composeTestRule.onNodeWithContentDescription(backText).performClick()
}

盡量提高測試涵蓋範圍

應用程式的測試套件應盡可能測試應用程式功能。在完美世界中,UI 測試套件會涵蓋 100% 的 UI 功能。實際上,由於應用程式外部許多可能影響 UI 的因素,例如具有不同螢幕大小的裝置、不同版本的 Android 作業系統,以及可能會影響手機上其他應用程式的第三方應用程式,所以很難實現大量測試涵蓋範圍。

如要盡可能提高測試涵蓋範圍,其中一種方式是在新增功能時一併撰寫測試。這樣一來,您就能避免過度掌握新功能,也不必反覆記住所有可能的情境。目前,紙杯蛋糕是相當小的應用程式,而您測試了應用程式導覽的一大部分!不過,還有其他要測試的導覽狀態。

確認是否可寫入測試以驗證下列導覽狀態。建議您先自行實作,再查看解決方案。

  • 按一下口味螢幕中的「向上」按鈕以前往「開始」螢幕
  • 按一下口味螢幕中的「取消」按鈕,以前往「開始」螢幕
  • 前往取貨畫面
  • 按一下「取貨」畫面中的「向上」按鈕,前往口味螢幕
  • 點選「取貨」畫面中的「取消」按鈕,即可前往「開始」螢幕
  • 前往「摘要」畫面
  • 按一下「摘要」畫面上的「取消」按鈕,前往「開始」螢幕。
@Test
fun cupcakeNavHost_clickNextOnFlavorScreen_navigatesToPickupScreen() {
   navigateToFlavorScreen()
   composeTestRule.onNodeWithStringId(R.string.next)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Pickup.name)
}

@Test
fun cupcakeNavHost_clickBackOnFlavorScreen_navigatesToStartOrderScreen() {
   navigateToFlavorScreen()
   performNavigateUp()
   navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickCancelOnFlavorScreen_navigatesToStartOrderScreen() {
   navigateToFlavorScreen()
   composeTestRule.onNodeWithStringId(R.string.cancel)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickNextOnPickupScreen_navigatesToSummaryScreen() {
   navigateToPickupScreen()
   composeTestRule.onNodeWithText(getFormattedDate())
       .performClick()
   composeTestRule.onNodeWithStringId(R.string.next)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Summary.name)
}

@Test
fun cupcakeNavHost_clickBackOnPickupScreen_navigatesToFlavorScreen() {
   navigateToPickupScreen()
   performNavigateUp()
   navController.assertCurrentRouteName(CupcakeScreen.Flavor.name)
}

@Test
fun cupcakeNavHost_clickCancelOnPickupScreen_navigatesToStartOrderScreen() {
   navigateToPickupScreen()
   composeTestRule.onNodeWithStringId(R.string.cancel)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

@Test
fun cupcakeNavHost_clickCancelOnSummaryScreen_navigatesToStartOrderScreen() {
   navigateToSummaryScreen()
   composeTestRule.onNodeWithStringId(R.string.cancel)
       .performClick()
   navController.assertCurrentRouteName(CupcakeScreen.Start.name)
}

6. 撰寫「訂單」畫面的測試

導覽只是 Cupcake 應用程式的一個方面。使用者與每個應用程式畫面互動。您必須確認這些畫面顯示的內容,並在這些螢幕上執行的動作符合正確的結果。SelectOptionScreen 是應用程式的重要部分。

在本節中,您將撰寫測試,確認這個畫面中的內容設定是否正確。

測試「選擇情境」畫面內容

  1. app/src/androidTest 目錄中建立名為 CupcakeOrderScreenTest 的新類別,在其他測試檔案中已存放。

b8d85fba1fabedef.png

  1. 在這個類別中,建立 AndroidComposeTestRule
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
  1. 建立名為 selectOptionScreen_verifyContent() 的函式,並以 @Test 加上註解。
@Test
fun selectOptionScreen_verifyContent() {

}

您會使用這個函式將 Compose 規則內容設為 SelectOptionScreen。這樣做可確保 SelectOptionScreen 可組合元件直接啟動,因此不需要瀏覽。不過,這個畫面需要兩個參數:小計和口味選項清單。

  1. 建立口味清單和要傳送至畫面的小計。
@Test
fun selectOptionScreen_verifyContent() {
   // Given list of options
   val flavours = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
   // And sub total
   val subTotal = "$100"
}
  1. 使用剛建立的值,將內容設為使用 CupcakeThemeSelectOptionScreen 可組合元件。

請注意,這個方法與從 MainActivity 啟動可組合函式類似。唯一的差別在於 MainActivity 呼叫 CupcakeApp可組合元件,在這裡呼叫 SelectOptionScreen 可組合元件。能夠變更從 setContent() 啟動的可組合元件,您可以啟動特定可組合元件,而不必明確測試應用程式,前往您要測試的區域。這種做法有助於避免在與目前測試無關的程式碼中失敗。

@Test
fun selectOptionScreen_verifyContent() {
   // Given list of options
   val flavours = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
   // And sub total
   val subTotal = "$100"

   // When SelectOptionScreen is loaded
   composeTestRule.setContent {
       CupcakeTheme {
           SelectOptionScreen(subtotal = subTotal, options = flavours)
       }
   }
}

在測試中,應用程式會啟動 SelectOptionScreen 可組合元件,然後您就可以透過測試操作說明與應用程式互動。

  1. 透過 flavours 清單疊代,確保清單中的每個字串項目都顯示在畫面上。
  2. 使用 onNodeWithText() 方法尋找畫面上的文字,並使用 assertIsDisplayed() 方法驗證文字是否顯示在應用程式中。
@Test
fun selectOptionScreen_verifyContent() {
   // Given list of options
   val flavours = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
   // And sub total
   val subTotal = "$100"

   // When SelectOptionScreen is loaded
   composeTestRule.setContent {
       CupcakeTheme {
           SelectOptionScreen(subtotal = subTotal, options = flavours)
       }
   }

   // Then all the options are displayed on the screen.
   flavours.forEach { flavour ->
       composeTestRule.onNodeWithText(flavour).assertIsDisplayed()
   }
}
  1. 使用相同技巧驗證應用程式顯示文字,確認應用程式在螢幕上顯示正確的小計字串。在畫面上搜尋 R.string.subtotal 資源 ID 和正確的小計值,然後宣告應用程式會顯示該值。
@Test
fun selectOptionScreen_verifyContent() {
   // Given list of options
   val flavours = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
   // And sub total
   val subTotal = "$100"

   // When SelectOptionScreen is loaded
   composeTestRule.setContent {
       CupcakeTheme {
           SelectOptionScreen(subtotal = subTotal, options = flavours)
       }
   }

   // Then all the options are displayed on the screen.
   flavours.forEach { flavour ->
       composeTestRule.onNodeWithText(flavour).assertIsDisplayed()
   }
   // And then the subtotal is displayed correctly.
   composeTestRule.onNodeWithText(
      composeTestRule.activity.getString(
          R.string.subtotal_price,
          subTotal
      )
   ).assertIsDisplayed()
}

請注意,你必須先選取項目,系統才會顯示下一步按鈕。這項測試只會驗證畫面內容,因此最後測試的方法是下一步按鈕。

  1. 使用相同方法尋找按字串資源 ID 尋找節點的下一步按鈕。不過,您可以使用 assertIsNotEnabled() 方法,而不是驗證應用程式顯示節點。
@Test
fun selectOptionScreen_verifyContent() {
   // Given list of options
   val flavours = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
   // And sub total
   val subTotal = "$100"

   // When SelectOptionScreen is loaded
   composeTestRule.setContent {
       CupcakeTheme {
           SelectOptionScreen(subtotal = subTotal, options = flavours)
       }
   }

   // Then all the options are displayed on the screen.
   flavours.forEach { flavour ->
       composeTestRule.onNodeWithText(flavour).assertIsDisplayed()
   }
   // And then the subtotal is displayed correctly.
   composeTestRule.onNodeWithText(
      composeTestRule.activity.getString(
          R.string.subtotal_price,
          subTotal
      )
   ).assertIsDisplayed()
   // And then the next button is disabled
composeTestRule.onNodeWithText(getString(R.string.next)).assertIsNotEnabled()
}

盡量提高測試涵蓋範圍

「選擇口味」畫面內容測試只能測試單一畫面的某方面。您可以撰寫一些額外的測試來提高程式碼涵蓋率。下載解決方案程式碼前,請先自行撰寫下列測試。

  • 驗證「開始」螢幕內容。
  • 驗證「摘要」螢幕內容。
  • 在「選擇口味」螢幕中選取選項時,確認已啟用下一步按鈕。

撰寫測試時,請注意任何 helper 函式都可能會減少整個撰寫過程中的程式碼數量!

7. 解決方案程式碼

8. 摘要

恭喜!您已瞭解如何測試 Jetpack 導覽元件。你也會學到撰寫 UI 測試的一些基本技能,例如撰寫可重複使用的 helper方法、如何利用 setContent() 撰寫簡潔測試、透過 @Before 註解設定測試,以及如何思考最高測試涵蓋率。繼續建構 Android 應用程式時,別忘了持續撰寫測試及功能程式碼!