相互運用性

Compose は一般的なテスト フレームワークと統合されています。

Espresso との相互運用

ハイブリッド アプリでは、ビュー階層内の Compose コンポーネントと Compose コンポーザブル内のビューを(AndroidView コンポーザブルを介して)見つけることができます。

どちらのタイプも、マッチングするために特別な手順は必要ありません。ビューをマッチングするには Espresso の onView を使用し、Compose 要素をマッチングするには ComposeTestRule を使用します。

@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state.
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state.
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value.
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}

Compose 相互運用テスト用にビュー スコープのセマンティクスを追加

Compose の検索を特定のビューに限定する

複雑な UI を Compose に移行する際、RecyclerView や ViewPager など、複数の従来の Android ビュー内にネストされた同一の Compose 要素に遭遇することがあります。このようなシナリオでは、onNodeWithText("Save") などの標準の Compose 検索が「複数のノードが見つかりました」というエラーで失敗することがあります。

これらの要素を区別するために動的テストタグを挿入するように本番環境コードを変更する代わりに、Compose テストのスコープを特定の Android ビューに直接設定できます。

テストルールで onRootWithViewInteraction API を使用します。この関数は Espresso の ViewInteraction を受け入れます。これにより、Espresso を利用して特定のコンテナ View を分離し、そのスコープ階層内でのみ Compose の操作を実行できます。

リスト項目を操作する

特定の RecyclerView 行内の Compose 要素を操作する必要がある場合は、Espresso を使用して行を特定し、Compose の操作をその行に限定します。これにより、他のすべての行の同一の Compose 要素は無視されます。

@Test
fun testComposeButtonInsideRecyclerViewItem() = runComposeUiTest {
    // Scroll to the desired position using Espresso
    Espresso.onView(withId(recyclerViewId))
        .perform(RecyclerViewActions.scrollToPosition<MyViewHolder>(3))

    // Define an Espresso ViewInteraction that uniquely identifies the row
    val rowView = Espresso.onView(
        allOf(
            withId(rootViewId),
            hasDescendant(withText("Item #3"))
        )
    )

    // Scope the Compose search strictly to that specific row View
    onRootWithViewInteraction(rowView)
        .onNode(hasText("Like"))
        .performClick()
}

ViewPager のあいまいさを解決する

同一の Compose レイアウトを持つ複数のフラグメントが同時にメモリ内にある場合は、特定のフラグメントのルートビュー ID に検索範囲を限定することで、一致の曖昧さを回避できます。

@Test
fun testComposeButtonInsideViewPagerItem() = runComposeUiTest {
    // Swipe to the desired page using Espresso
    Espresso.onView(withId(viewPagerViewId)).perform(swipeLeft())

    // Identify the specific container view using Espresso
    val fragmentB = Espresso.onView(withId(fragmentRootViewId))

    // The generic text "Save" is now unique within this view scope
    onRootWithViewInteraction(fragmentB)
        .onNode(hasText("Save"))
        .assertIsDisplayed()
}

UiAutomator との相互運用

デフォルトでは、使い勝手のよい記述子(表示されるテキストやコンテンツの説明など)によってのみ、UiAutomator からコンポーザブルにアクセスできます。Modifier.testTag を使用する任意のコンポーザブルにアクセスするには、そのコンポーザブルのサブツリーに対してセマンティック プロパティ testTagsAsResourceId を有効にする必要があります。この動作を有効にすると、スクロール可能なコンポーザブル(LazyColumn など)のように、一意のハンドルがないコンポーザブルに役立ちます。

コンポーザブルの階層では、上位で 1 度セマンティクス プロパティを有効にすれば、Modifier.testTag を持つすべてのネストされたコンポーザブルに UiAutomator からアクセスできるようになります。

Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // Content
    }
}

Modifier.testTag(tag) を持つ任意のコンポーザブルにアクセスするには、By.res(resourceName) で resourceName と同じ tag を使用します。

val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// Some interaction with the lazyColumn.

参考情報

  • Android でアプリをテストする: Android テストのメイン ランディング ページで、テストの基礎と手法についてより広範な視点から説明しています。
  • テストの基礎: Android アプリのテストの背景にある基本概念について詳しく学べます。
  • ローカルテスト: 一部のテストは、独自のワークステーションでローカルに実行できます。
  • インストルメンテーション テスト: インストルメンテーション テストも実行することをおすすめします。つまり、デバイス上で直接実行されるテストです。
  • 継続的インテグレーション: 継続的インテグレーションを使用すると、テストをデプロイ パイプラインに統合できます。
  • さまざまな画面サイズをテストする: ユーザーが利用できるデバイスは多岐にわたるため、さまざまな画面サイズでテストする必要があります。
  • Espresso: ビューベースの UI を対象としていますが、Espresso の知識は Compose テストのいくつかの側面で役立ちます。