Cupcake アプリをテストする

1. はじめに

Compose で画面間を移動する」Codelab では、Jetpack Navigation Compose コンポーネントを使用して、Compose アプリにナビゲーションを追加する方法を学習しました。

Cupcake アプリには複数の画面があり、ユーザーは移動し、さまざまなアクションを行えます。このアプリは、自動テストのスキルを磨くのに適しています。この Codelab では、Cupcake アプリの UI テストをいくつか作成し、テスト カバレッジを最大化する方法について学習します。

前提条件

学習内容

  • Compose で Jetpack Navigation コンポーネントをテストする。
  • UI テストごとに一貫した UI 状態を作成する。
  • テスト用のヘルパー関数を作成する。

作成するアプリの概要

  • Cupcake アプリの UI テスト

必要なもの

  • Android Studio の最新バージョン
  • スターター コードをダウンロードするためのインターネット接続

2. スターター コードをダウンロードする

  1. Android Studio で basic-android-kotlin-compose-training-cupcake フォルダを開きます。
  2. Android Studio で Cupcake アプリのコードを開きます。

3. UI テスト用に Cupcake をセットアップする

androidTest の依存関係を追加する

Gradle ビルドツールを使用すると、特定のモジュールの依存関係を追加できます。この機能により、依存関係が不必要にコンパイルされなくなります。プロジェクトに依存関係を追加する際の implementation 構成については、すでにご存じでしょう。このキーワードを使用して、アプリ モジュールの build.gradle.kts ファイルで依存関係をインポートしました。implementation キーワードを使用すると、そのモジュール内のすべてのソースセットでその依存関係を利用できるようになります。このコースではこれまで、maintestandroidTest ソースセットを扱いました。

UI テストは、androidTest という独自のソースセットに含まれています。このモジュールにのみ必要な依存関係は、アプリコードが含まれている main モジュールなど、他のモジュール用にコンパイルする必要はありません。UI テストでのみ使用する依存関係を追加する場合は、androidTestImplementation キーワードを使用して、アプリ モジュールの build.gradle.kts ファイルで依存関係を宣言します。そうすることで、UI テストを実行したときだけ UI テストの依存関係がコンパイルされるようになります。

次の手順で、UI テストの作成に必要な依存関係を追加します。

  1. build.gradle.kts(Module :app) ファイルを開きます。
  2. このファイルの dependencies セクションに、次の依存関係を追加します。
androidTestImplementation(platform("androidx.compose:compose-bom:2023.05.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.navigation:navigation-testing:2.6.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")

UI テスト ディレクトリを作成する

  1. プロジェクト ビューで src ディレクトリを右クリックし、[New] > [Directory] を選択します。

31f950996e51807b.png

  1. [androidTest/java] オプションを選択します。

e1e4d108c863ae27.png

テスト パッケージを作成する

  1. プロジェクト ウィンドウで androidTest/java ディレクトリを右クリックし、[New] > [Package] を選択します。

c5cdd7125e61bf22.png

  1. パッケージに「com.example.cupcake.test」という名前を付けます。b6cec61624467422.png

ナビゲーション テストクラスを作成する

test ディレクトリで、CupcakeScreenNavigationTest という新しい Kotlin クラスを作成します。

8da2547b2ef1cc8e.png

4. ナビゲーション ホストをセットアップする

以前 Codelab では、Compose での UI テストには Compose テストルールが必要であることを学習しました。Jetpack Navigation のテストも同様です。ただしナビゲーションをテストする場合は、Compose テストルールによる追加のセットアップが必要です。

Compose Navigation をテストする場合、アプリコードと同じ NavHostController にはアクセスできません。ただし、TestNavHostController を使用し、このナビゲーション コントローラでテストルールを構成することはできます。このセクションでは、ナビゲーション テストのテストルールを構成、再利用する方法について説明します。

  1. CupcakeScreenNavigationTest.kt で、createAndroidComposeRule を使用し ComponentActivity を型パラメータとして渡すテストルールを作成します。
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.junit.Rule

@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()

アプリを正しい場所に移動させるには、アプリが移動するためのアクションを行ったときに、TestNavHostController インスタンスを参照してナビゲーション ホストのナビゲーション ルートを確認する必要があります。

  1. TestNavHostController インスタンスを lateinit 変数としてインスタンス化します。Kotlin では、lateinit キーワードを使用して、オブジェクトの宣言後に初期化できるプロパティを宣言します。
import androidx.navigation.testing.TestNavHostController

private lateinit var navController: TestNavHostController

次に、UI テストに使用するコンポーザブルを指定します。

  1. setupCupcakeNavHost() というメソッドを作成します。
  2. setupCupcakeNavHost() メソッドで、作成した Compose テストルールに対して setContent() メソッドを呼び出します。
  3. setContent() メソッドに渡されたラムダ内で、CupcakeApp() コンポーザブルを呼び出します。
import com.example.cupcake.CupcakeApp

fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        CupcakeApp()
    }
}

今度は、テストクラスに TestNavHostContoller オブジェクトを作成する必要があります。アプリはコントローラを使用して Cupcake アプリのさまざまな画面を移動するため、このオブジェクトはナビゲーション状態を判断するために後で使用します。

  1. 前に作成したラムダを使用して、ナビゲーション ホストをセットアップします。作成した navController 変数を初期化し、ナビゲータを登録して、その TestNavHostControllerCupcakeApp コンポーザブルに渡します。
import androidx.compose.ui.platform.LocalContext

fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
        }
        CupcakeApp(navController = navController)
    }
}

CupcakeScreenNavigationTest クラスのテストはすべて、ナビゲーションの面をテストします。そのため、各テストは作成した TestNavHostController オブジェクトに依存します。テストのたびに setupCupcakeNavHost() 関数を手動で呼び出してナビゲーション コントローラをセットアップするのではなく、junit ライブラリが提供する @Before アノテーションを使用して自動的に行うことができます。メソッドに @Before アノテーションを付けると、@Test アノテーションを付けたどのメソッドよりも前に実行されます。

  1. @Before アノテーションを setupCupcakeNavHost() メソッドに追加します。
import org.junit.Before

@Before
fun setupCupcakeNavHost() {
    composeTestRule.setContent {
        navController = TestNavHostController(LocalContext.current).apply {
            navigatorProvider.addNavigator(ComposeNavigator())
        }
        CupcakeApp(navController = navController)
    }
}

5. ナビゲーション テストを作成する

開始デスティネーションを検証する

Cupcake アプリを作成したとき、アプリのナビゲーションを示す定数を含む CupcakeScreen という enum クラスを作成しました。

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 アイテムを使用して、この画面に移動するタイミングを決定します。アプリの起動時に、デスティネーション ルートがまだ存在しない場合、ナビゲーション ホストのデスティネーション ルートは CupcakeScreen.Start.name に設定されます。

まず、アプリの起動時に注文開始画面が現在のデスティネーション ルートであることを検証するテストを作成する必要があります。

  1. cupcakeNavHost_verifyStartDestination() という関数を作成し、@Test アノテーションを付けます。
import org.junit.Test

@Test
fun cupcakeNavHost_verifyStartDestination() {
}

ここで、ナビゲーション コントローラの最初のデスティネーション ルートが注文開始画面であることを確認します。

  1. 想定されるルート名(この場合は CupcakeScreen.Start.name)が、ナビゲーション コントローラの現在のバックスタック エントリのデスティネーション ルートと等しいことをアサートします。
import org.junit.Assert.assertEquals
...

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

ヘルパー メソッドを作成する

UI テストでは、UI の特定部分をテストできる状態にするために、手順を繰り返す必要が生じることがよくあります。カスタム UI では、複数のコード行を必要とする複雑なアサーションも必要になる場合があります。前のセクションで記述したアサーションは、多くのコードを必要とします。Cupcake アプリでナビゲーションをテストするときは、同じアサーションを何度も使用します。このような場合は、テストにヘルパー メソッドを作成すると、コードを重複して記述せずに済みます。

ナビゲーション テストを作成するたびに、CupcakeScreen 列挙型アイテムの name プロパティを使用して、ナビゲーション コントローラの現在のデスティネーション ルートが正しいことを確認します。このようなアサーションを行うときにいつでも呼び出せるヘルパー関数を作成します。

次の手順でヘルパー関数を作成します。

  1. test ディレクトリに ScreenAssertions という空の Kotlin ファイルを作成します。

8c3c7146050db2a8.png

  1. NavController クラスに assertCurrentRouteName() という拡張関数を追加し、メソッドのシグネチャに想定されるルート名の文字列を渡します。
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)
}

多くのテストでは、UI コンポーネントの操作も必要になります。この Codelab ではリソース文字列を使用して、こうしたコンポーネントを見つけることがよくあります。コンポーザブルにアクセスするには、リソース文字列と Context.getString() メソッドを使用します。詳しくは、こちらをご覧ください。Compose で UI テストを記述する場合、このメソッドの実装は次のようになります。

composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.my_string)

これは詳細な命令であり、拡張関数を追加することで簡略化できます。

  1. com.example.cupcake.test パッケージに ComposeRuleExtensions.kt という名前の新しいファイルを作成します。プレーンな Kotlin ファイルであることを確認してください。

82762f66f890cf1b.png

  1. このファイルに次のコードを追加します。
import androidx.activity.ComponentActivity
import androidx.annotation.StringRes
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.rules.ActivityScenarioRule

fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.onNodeWithStringId(
    @StringRes id: Int
): SemanticsNodeInteraction = onNodeWithText(activity.getString(id))

この拡張関数を使用すると、UI コンポーネントを文字列リソースで見つける際に記述するコードの量を減らせます。次の記述を参考に説明します。

composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.my_string)

これを、次のような命令として使用できるようになります。

composeTestRule.onNodeWithStringId(R.string.my_string)

スタート画面に上ボタンがないことを検証する

Cupcake アプリの元のデザインでは、開始画面のツールバーに「上へ」ボタンがありません。

886fb7e824608c92.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()
}

フレーバー画面へのナビゲーションを検証する

開始画面でボタンをクリックすると、ナビゲーション コントローラにレシーバー画面への移動を指示するメソッドがトリガーされます。

82c62c13891d627e.png

このテストでは、ボタンをクリックしてこのナビゲーションをトリガーし、デスティネーション ルートがフレーバー画面であることを検証するコマンドを作成します。

  1. cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen() という関数を作成し、@Test アノテーションを付けます。
@Test
fun cupcakeNavHost_clickOneCupcake_navigatesToSelectFlavorScreen(){
}
  1. 文字列リソース ID で [One Cupcake] ボタンを探し、クリック アクションを行います。
import com.example.cupcake.R
...

@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)
}

その他のヘルパー メソッドを作成する

Cupcake アプリのナビゲーション フローはほぼ直線的です。[Cancel] ボタンをクリックした場合を除き、アプリ内を一方向にしか移動できません。そのため、アプリ内の深い画面をテストする場合、テストするエリアに移動するためのコードを繰り返すことになります。この状況では、コードを 1 回記述するだけで済むようにヘルパー メソッドを多く使用する価値があります。

フレーバー画面へのナビゲーションをテストしたので、今後のテストで同じコードを繰り返す必要がなくなるよう、フレーバー画面に移動するメソッドを作成しましょう。

  1. navigateToFlavorScreen() というメソッドを作成します。
private fun navigateToFlavorScreen() {
}
  1. 前のセクションと同様に、[One Cupcake] ボタンを探してクリック アクションを行うコマンドを記述します。
private fun navigateToFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
}

フレーバー画面の [Next] ボタンは、フレーバーを選択するまでクリックできませんでした。このメソッドは、あくまでもナビゲーション用の UI を準備するためのものです。このメソッドを呼び出すと、UI は [Next] ボタンをクリックできる状態になります。

  1. UI で R.string.chocolate 文字列を含むノードを探し、クリック アクションを行って選択します。
private fun navigateToFlavorScreen() {
    composeTestRule.onNodeWithStringId(R.string.one_cupcake)
        .performClick()
    composeTestRule.onNodeWithStringId(R.string.chocolate)
        .performClick()
}

受け取り画面と概要画面に移動するヘルパー メソッドを作成できるかどうかを確認しましょう。解答を見る前に、この演習をご自身で試してみてください。

そのためには、次のコードを使用します。

private fun getFormattedDate(): String {
    val calendar = Calendar.getInstance()
    calendar.add(java.util.Calendar.DATE, 1)
    val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
    return formatter.format(calendar.time)
}
private fun navigateToPickupScreen() {
    navigateToFlavorScreen()
    composeTestRule.onNodeWithStringId(R.string.next)
        .performClick()
}

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

開始画面以外の画面をテストする場合、「上へ」ボタンの機能をテストして、前の画面に移動することを確認する計画を立てる必要があります。「上へ」ボタンを見つけてクリックするためにヘルパー関数を作成することを検討してください。

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

テスト カバレッジを最大化する

アプリのテストスイートは、アプリの機能を可能な限りテストする必要があります。UI テストスイートで UI 機能を 100% カバーすることが理想です。実際には、UI に影響を与える可能性のあるアプリの外部要因(固有の画面サイズを持つデバイス、Android オペレーティング システムのバージョンの違い、スマートフォン上の他のアプリに影響を与える可能性のあるサードパーティ アプリなど)が数多く存在するため、それだけのテスト カバレッジを達成することは困難です。

テスト カバレッジを最大化する方法として、機能を追加すると同時にテストを作成することが挙げられます。そうすることで、新機能に対して先回りしすぎることや、考えられるシナリオをすべて思い出すために後戻りしなければならなくなることを避けられます。この時点では Cupcake アプリはかなり小さいため、アプリのナビゲーションの大部分はテスト済みです。しかし、テストするナビゲーション状態はまだあります。

以下のナビゲーション状態を検証するためのテストを作成できるかどうかを確認しましょう。解答を見る前に、ご自身で実装してみてください。

  • フレーバー画面から「上へ」ボタンをクリックして開始画面に移動する
  • フレーバー画面から [Cancel] ボタンをクリックして開始画面に移動する
  • 受け取り画面に移動する
  • 受け取り画面から「上へ」ボタンをクリックしてフレーバー画面に移動する
  • 受け取り画面から [Cancel] ボタンをクリックして開始画面に移動する
  • 概要画面に移動する
  • 概要画面から [Cancel] ボタンをクリックして開始画面に移動する
@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 という新しいクラスを作成します。

4409b8333eff0ba2.png

  1. このクラスで、AndroidComposeTestRule を作成します。
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
  1. selectOptionScreen_verifyContent() という関数を作成し、@Test アノテーションを付けます。
@Test
fun selectOptionScreen_verifyContent() {

}

この関数では、Compose ルールの内容を最終的に SelectOptionScreen に設定します。これにより、SelectOptionScreen コンポーザブルが直接起動されるため、ナビゲーションが不要になります。ただし、この画面にはフレーバー オプションのリストと小計の 2 つのパラメータが必要です。

  1. 画面に渡すフレーバー オプションのリストと小計を作成します。
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"
}
  1. 作成した値を使用して、SelectOptionScreen コンポーザブルにコンテンツを設定します。

なお、このアプローチは MainActivity からコンポーザブルを起動する場合と同様です。唯一の違いは、MainActivityCupcakeApp コンポーザブルを呼び出すのに対して、ここでは SelectOptionScreen コンポーザブルを呼び出している点です。setContent() から起動するコンポーザブルを変更できるため、テストするエリアに到達するためにアプリを明示的に順を追って操作するのではなく、特定のコンポーザブルを起動できます。このアプローチでは、現在のテストと無関係なコードのエリアでテストが失敗することを避けられます。

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

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

テストのこの時点で、アプリが SelectOptionScreen コンポーザブルを起動し、テストの手順に沿って操作できるようになります。

  1. flavors リストを反復処理し、リスト内の各文字列アイテムが画面に表示されることを確認します。
  2. onNodeWithText() メソッドを使用して画面上のテキストを見つけ、assertIsDisplayed() メソッドを使用してテキストがアプリに表示されることを検証します。
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

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

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }
}
  1. 同じ手法で、アプリにテキストが表示されることを検証し、アプリで正しい小計の文字列が画面に表示されることを検証します。画面で R.string.subtotal_price リソース ID と正しい小計値を検索し、アプリがその値を表示することをアサートします。
import com.example.cupcake.R
...

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

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

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).assertIsDisplayed()
    }

    // And then the subtotal is displayed correctly.
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(
            R.string.subtotal_price,
            subtotal
        )
    ).assertIsDisplayed()
}

[Next] ボタンは、アイテムを選択するまで有効になりませんでした。このテストでは画面コンテンツのみを検証するため、最後に [Next] ボタンが無効になっていることをテストします。

  1. 文字列リソース ID でノードを探す場合と同じアプローチで [Next] ボタンを探します。ただし、アプリにノードが表示されることを確認するのではなく、assertIsNotEnabled() メソッドを使用します。
@Test
fun selectOptionScreen_verifyContent() {
    // Given list of options
    val flavors = listOf("Vanilla", "Chocolate", "Hazelnut", "Cookie", "Mango")
    // And subtotal
    val subtotal = "$100"

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

    // Then all the options are displayed on the screen.
    flavors.forEach { flavor ->
        composeTestRule.onNodeWithText(flavor).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.onNodeWithStringId(R.string.next).assertIsNotEnabled()
}

テスト カバレッジを最大化する

フレーバー選択画面のコンテンツのテストは、1 つの画面の 1 つの要素だけをテストします。コード カバレッジを広げるために作成できるテストは他にもあります。解答コードをダウンロードする前に、以下のテストをご自身で記述してみてください。

  • 開始画面のコンテンツを検証する。
  • 概要画面のコンテンツを検証する。
  • フレーバー選択画面でオプションが選択されたら [Next] ボタンが有効になることを検証する。

テストを作成するときは、コードの記述量を減らせるヘルパー関数がないか注意してください。

7. 解答コードを取得する

この Codelab の完成したコードをダウンロードするには、次の git コマンドを使用します。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

解答コードを確認する場合は、GitHub で表示します

8. まとめ

お疲れさまでした。Jetpack Navigation コンポーネントをテストする方法について学習しました。また、再利用可能なヘルパー メソッドを記述する方法、setContent() を活用して簡潔なテストを作成する方法、@Before アノテーションを付けてテストをセットアップする方法、最大のテスト カバレッジについての考え方など、UI テストを作成するための基本的なスキルについても学習しました。今後も Android アプリを作成する際、機能のコードとともにテストも忘れずに作成してください。