Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

Compose レイアウトのテスト

Compose に基づく UI のテストは、ビューベースの UI のテストとは異なります。ビューベースの UI ツールキットは、ビューとは何かを明確に定義します。ビューは長方形の空間を占有し、ウィジェットまたはレイアウトにすることができます。ビューには、識別子、位置、マージン、パディングなどのプロパティがあります。

Compose は別のアプローチをとります。View 要素ではなく、UI を出力するコンポーズ可能な関数を定義します。コンポーザブルには、ID またはコンテンツの説明はありません。では、UI テストでボタンをクリックするなどの操作をどのように行うのでしょうか。このドキュメントでは、Compose の同等のアプローチについて説明します。

セマンティクス

Compose の UI テストでは、セマンティクスを使用して UI 階層を操作します。セマンティクスは、その名のとおり、UI の一部に意味を与えます。ここで、「UI の一部」(要素)とは、単一のコンポーザブルから画面全体まで、あらゆるものを指します。セマンティクス ツリーは UI 階層に沿って生成、記述されます。

典型的な UI レイアウトと、対応するセマンティクス ツリーにレイアウトがどのようにマッピングされるかを示す図

図 1. UI 階層とそのセマンティクス ツリー

セマンティクス フレームワークは主にユーザー補助機能で使用されるため、テストでは、UI 階層に関するセマンティクスによって公開された情報を利用します。公開する内容と公開範囲はデベロッパーが決定します。

画像とテキストを含むボタン

図 2. アイコンとテキストを含む一般的なボタン。

たとえば、アイコンとテキスト要素で構成されるこのようなボタンの場合、デフォルトのセマンティクス ツリーには「Like」というテキストラベルしか含まれません。これは、Text のようなコンポーザブルの中には、プロパティがすでにセマンティクス ツリーに公開されているものがあるためです。プロパティをセマンティクス ツリーに追加するには、Modifier を使用します。

Text(modifier = Modifier.semantics { text = text } )

デフォルトのセマンティック出力をオーバーライドできます。たとえば、この場合、ボタンで「Like button」というテキストラベルを持つノードを 1 つ出力させることができます。

MyButton(modifier = Modifier.semantics { text = "Like button" } )

設定

このセクションでは、Compose コードをテストできるようにモジュールを設定する方法について説明します。

まず、UI テストを含むモジュールの build.gradle ファイルに次の依存関係を追加します。

androidTestImplementation("androidx.ui:ui-test:$compose_version")

このモジュールには、ComposeTestRule と、AndroidComposeTestRule と呼ばれる Android 用の実装が含まれています。このルールを通じて、Compose コンテンツを設定したり、アクティビティにアクセスしたりできます。Compose の一般的な UI テストは次のようになります。

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MyActivity>()
    // createComposeRule() if you don't need access to the activityTestRule

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, ...)
            }
        }

        onNodeWithText("Continue").performClick()

        onNodeWithText("Welcome").assertIsDisplayed()
    }
}

createComposeRule を使用する場合は、ComponentActivity を Android マニフェストに追加する必要があります。これは app/src/debug/AndroidManifest.xml で行うことをおすすめします。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <activity android:name="androidx.activity.ComponentActivity" />
    </application>
</manifest>

テスト API

Espresso を使用したことがある場合は、Compose のテスト API も同じような感覚で利用できます。

Compose は SemanticsMatcher マッチャーを使用して、セマンティクス ツリー内の 1 つ以上のノードを参照します。1 つ以上のノードと一致したら、そのノードに対してアクションを実行したり、アサーションを行ったりできます。

ファインダー

ノードを 1 つ選択する

onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

ノードを複数選択する

onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

マージされていないツリーを使用する

一部のノードでは、子のセマンティクス情報がマージされます。たとえば、2 つのテキスト要素を含むボタンでは、ラベルがマージされます。

Button { Text("Hello"), Text("World") }

テストから、printToLog() を使用してセマンティクス ツリーを表示できます。

onRoot().printToLog("TAG")

このコードでは、次の出力が表示されます。

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   Text = Hello World
   MergeDescendants = 'true'

マージされていないツリーのノードと一致させる必要がある場合は、useUnmergedTreetrue に設定します。

onRoot(useUnmergedTree = true).printToLog("TAG")

このコードでは、次の出力が表示されます。

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hello'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'World'

useUnmergedTree パラメータはすべてのファインダーで使用できます。たとえば onNodeWithText ファインダーでは次のように使用されます。

onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

アサーション

1 つ以上のマッチャーを使用して、ファインダーで返される SemanticsNodeInteraction に対して assert() を呼び出すことで、アサーションを確認します。

// Single matcher:
onNode(...).assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
onNode(...).assert(hasText("Button") or hasText("Button2"))

ノードのコレクションでアサーションを確認する関数もあります。

// Check number of matched nodes
onAllNodesWithLabel("Beatle").assertCountEquals(4)
// At least one matches
onAllNodesWithLabel("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
onAllNodesWithLabel("Beatle").assertAll(assertHasClickAction())

アクション

ノードにアクションを挿入するには、perform…() 関数を呼び出します。

onNode(...).performClick()

アクションの例を次に示します。

performClick(),
performSemanticsAction(),
performKeyPress(),
performGesture { swipeLeft() }

マッチャー

このセクションでは、Compose コードのテストに使用できる一部のマッチャーについて説明します。

階層マッチャー

階層マッチャーを使用すると、セマンティクス ツリーを上下に移動して、シンプルなマッチングを実施できます。

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

こうしたマッチャーの例を次に示します。

onNode(hasParent(hasText("Button"))
    .assertIsVisible()

セレクタ

テストを作成する別の方法として、セレクタを使用する方法があります。これにより、一部のテストが読みやすくなります。

onNode(hasTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

一般的なパターン

このセクションでは、Compose テストでよく見られる一般的なアプローチについて説明します。

単独でのテスト

ComposeTestRule を使用すると、任意のコンポーザブル(アプリケーション全体、単一の画面、ウィジェットなど)を表示するアクティビティを開始できます。また、コンポーザブルが正しくカプセル化されているか、独立して動作するかを確認することをおすすめします。これにより、UI テストをより簡単かつ集中的に行うことができます。

これは、単体 UI テストのみを作成するということではありません。UI テストでは、UI の大きな部分にスコープ設定することも非常に重要です。

構成変更のシミュレーション

Compose の構成は、アンビエントで提供されます。アンビエントは、アプリが動作するデバイスの特性を記述します。構成が変更されるたびに、アンビエントはツリーの関連部分を再コンポーズします。構成には、画面のサイズや向き、夜間モードなどの情報が含まれます。こうしたパラメータの変更をデバイスに求めるよりも、Compose で構成の変更をシミュレートする方がはるかに迅速で、信頼性が高くなります。

class MyTest() {

    private val themeIsDark = MutableStateFlow(false)

    @Before
    fun setUp() {
        composeTestRule.setContent {
            JetchatTheme(
              isDarkTheme = themeIsDark.collectAsState(false).value
            ) {
                MainScreen()
            }
        }
    }

    @Test
    fun changeTheme_scrollIsPersisted() {
        findByLabel("Continue").doClick()

        // Set theme to dark
        themeIsDark.value = true

        // Check that we're still on the same page
        findByLabel("Welcome").assertIsDisplayed()
    }
}

デバッグ

テストで問題を解決する主な方法は、セマンティクス ツリーを確認することです。テストの任意の時点で findRoot().printToLog() を呼び出すことで、ツリーを出力できます。この関数は次のようなログを出力します。

    Node #1 at (...)px
     |-Node #2 at (...)px
       OnClick = '...'
       MergeDescendants = 'true'
        |-Node #3 at (...)px
        | Text = 'Hi'
        |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
          Text = 'There'

こうしたログには、バグを追跡するための貴重な情報が含まれています。

アニメーションの無効化

テストがアニメーションの正しい動作をチェックしている場合を除き、テストを可能な限り迅速に行えるように、アニメーションを無効にすることをおすすめします。テストはアニメーションによって大幅に遅くなる可能性があるため、AndroidComposeTestRule にはすべてのアニメーションを無効にするパラメータが用意されています。

@get:Rule
val composeTestRule =
  AndroidComposeTestRule<MyActivity>(disableTransitions = true)

トランジションとアニメーションのシステム設定を開発者向けオプションで行う必要はなくなりました。