UI または画面のテストは、Compose コードの動作が適切かどうかを検証し、開発プロセスの早い段階でエラーを捉えてアプリの品質を向上させるために使用します。
Compose には、要素を検索し、属性を検証して、ユーザー アクションを実行するためのテスト API が用意されています。また、時間操作などの高度な機能も含まれます。
セマンティクス
Compose の UI テストでは、セマンティクスを使用して UI 階層を操作します。セマンティクスは、その名のとおり、UI の一部に意味を与えます。ここで、「UI の一部」(要素)とは、単一のコンポーザブルから画面全体まで、あらゆるものを指します。セマンティクス ツリーは UI 階層に沿って生成、記述されます。
図 1. 典型的な UI 階層とそのセマンティクス ツリー。
セマンティクス フレームワークは主にユーザー補助機能で使用されるため、テストでは、セマンティクスが UI 階層について公開する情報を利用します。公開する内容と公開範囲はデベロッパーが決定します。
図 2. アイコンとテキストを含む一般的なボタン。
たとえば、アイコンとテキスト要素で構成されるこのようなボタンの場合、デフォルトのセマンティクス ツリーには「Like」というテキストラベルしか含まれません。これは、一部のコンポーザブル(Text
など)がすでにセマンティクス ツリーに一部のプロパティを公開しているためです。プロパティをセマンティック ツリーに追加するには、Modifier
を使用します。
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
設定
このセクションでは、Compose コードをテストできるようにモジュールを設定する方法について説明します。
まず、UI テストを含むモジュールの build.gradle
ファイルに次の依存関係を追加します。
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
このモジュールには、ComposeTestRule
と、AndroidComposeTestRule
と呼ばれる Android 用の実装が含まれています。このルールを通じて、Compose コンテンツを設定したり、アクティビティにアクセスしたりできます。Compose の一般的な UI テストは次のようになります。
// file: app/src/androidTest/java/com/package/MyComposeTest.kt
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = fakeUiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
テスト API
要素に対して行う操作は主に 3 つあります。
- ファインダーを使用し、アサーションやアクションを行う 1 つまたは複数の要素(セマンティクス ツリー内のノード)を選択する。
- アサーションにより、要素が存在することや特定の属性を持つことを確認する。
- アクションにより、要素に対するクリックやジェスチャーなどのユーザー イベントをシミュレートする。
これらの API の一部は、SemanticsMatcher
に対応していて、セマンティクス ツリーの 1 つ以上のノードを参照できます。
ファインダー
onNode
では 1 つのノードを、onAllNodes
では複数のノードを選択できますが、onNodeWithText
や onNodeWithContentDescription
などの便利なファインダーを使って一般的な検索を行うこともできます。完全なリストについては、Compose テスト クイック リファレンスをご覧ください。
ノードを 1 つ選択する
composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
.onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
ノードを複数選択する
composeTestRule
.onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
.onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
マージされていないツリーを使用する
一部のノードでは、子のセマンティクス情報がマージされます。たとえば、2 つのテキスト要素を含むボタンでは、ラベルがマージされます。
MyButton {
Text("Hello")
Text("World")
}
テストから、printToLog()
を使用してセマンティクス ツリーを表示できます。
composeTestRule.onRoot().printToLog("TAG")
このコードでは、次の出力が表示されます。
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
マージされていないツリーのノードと一致させる必要がある場合は、useUnmergedTree
を true
に設定します。
composeTestRule.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
ファインダーでは次のように使用されます。
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
アサーション
1 つ以上のマッチャーを使用して、ファインダーで返される SemanticsNodeInteraction
に対して assert()
を呼び出すことで、アサーションを確認します。
// Single matcher:
composeTestRule
.onNode(matcher)
.assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
composeTestRule
.onNode(matcher).assert(hasText("Button") or hasText("Button2"))
一般的なアサーションには、assertExists
、assertIsDisplayed
、assertTextEquals
などの便利な関数も使用できます。完全なリストについては、Compose テスト クイック リファレンスをご覧ください。
ノードのコレクションでアサーションを確認する関数もあります。
// Check number of matched nodes
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
アクション
ノードにアクションを挿入するには、perform…()
関数を呼び出します。
composeTestRule.onNode(...).performClick()
アクションの例を次に示します。
performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }
完全なリストについては、Compose テスト クイック リファレンスをご覧ください。
マッチャー
このセクションでは、Compose コードのテストに使用できる一部のマッチャーについて説明します。
階層マッチャー
階層マッチャーを使用すると、セマンティクス ツリーを上下に移動して、シンプルなマッチングを実施できます。
fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher
こうしたマッチャーの例を次に示します。
composeTestRule.onNode(hasParent(hasText("Button")))
.assertIsDisplayed()
セレクタ
テストを作成する別の方法として、セレクタを使用する方法があります。これにより、一部のテストが読みやすくなります。
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
完全なリストについては、Compose テスト クイック リファレンスをご覧ください。
同期
Compose テストは、デフォルトで UI と同期されます。ComposeTestRule
を使用してアサーションまたはアクションを呼び出すと、テストは事前に同期され、UI ツリーがアイドル状態になるのを待機します。
通常は、何もする必要はありません。ただし、知っておくべきエッジケースがいくつかあります。
テストが同期されると、Compose アプリは仮想クロックを使用して時間を進めます。つまり、Compose テストはリアルタイムで実行されないため、可能な限り早く結果を出すことができます。
ただし、テストを同期するメソッドを使用しなかった場合は、再コンポジションが発生せず、UI が一時停止しているように見えます。
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions
var lastSeenValue = 0 // Used to track recompositions
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition
// Fails because nothing triggered a recomposition
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition
composeTestRule.onNodeWithText("1").assertExists()
}
この要件が適用されるのは Compose 階層のみで、アプリの他の部分は適用対象外であることにも注意してください。
自動同期を無効にする
assertExists()
などの ComposeTestRule
を介してアサーションまたはアクションを呼び出すと、テストは Compose UI と同期されます。場合によっては、この同期を停止して、手動でクロックを制御できます。たとえば、UI がまだビジー状態である時点で、アニメーションの正確なスクリーンショットを撮る時間を制御できます。自動同期を無効にするには、mainClock
の autoAdvance
プロパティを false
に設定します。
composeTestRule.mainClock.autoAdvance = false
この場合、通常は手動で時間を進めます。advanceTimeByFrame()
を使用してフレームを正確に 1 つだけ進めたり、advanceTimeBy()
を使用して進める時間を指定したりできます。
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
アイドリング リソース
Compose は、テストと UI を同期することにより、すべてのアクションとアサーションがアイドル状態で実行され、必要に応じてクロックを待機させるか進めるようにすることができます。ただし、結果が UI 状態に影響する一部の非同期オペレーションは、テストによって認識されていないときにバックグラウンドで実行される可能性があります。
このようなアイドリング リソースをテスト内で作成して登録すると、テスト対象のアプリがビジー状態かアイドル状態かを判断する際に、それらのリソースが考慮されます。Espresso または Compose と同期されないバックグラウンド ジョブを実行する場合など、追加のアイドリング リソースを登録する必要がない場合は、何もする必要はありません。
この API は Espresso のアイドリング リソースとよく似ており、テスト対象がアイドル状態かビジー状態かを示します。IdlingResource
の実装を登録するには、Compose テストルールを使用します。
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
手動同期
特定のケースでは、Compose UI をテストの他の部分またはテスト対象のアプリと同期する必要があります。
waitForIdle
は Compose がアイドル状態になるのを待機しますが、autoAdvance
プロパティに依存します。
composeTestRule.mainClock.autoAdvance = true // default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for Idling Resources to become idle
どちらの場合も、waitForIdle
は保留中の描画パスとレイアウトパスも待機します。
また、advanceTimeUntil()
を使用して、特定の条件が満たされるまでクロックを進めることもできます。
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
特定の条件は、このクロックに影響される可能性がある状態をチェックするものでなければなりません(これは Compose 状態でのみ機能します)。Android の測定または描画(つまり、Compose の外部の測定または描画)に依存する条件では、waitUntil()
のようなより一般的なコンセプトを採用する必要があります。
composeTestRule.waitUntil(timeoutMs) { condition }
一般的なパターン
このセクションでは、Compose テストでよく見られる一般的なアプローチについて説明します。
単独でのテスト
ComposeTestRule
を使用すると、任意のコンポーザブル(アプリケーション全体、単一の画面、小さな要素など)を表示するアクティビティを開始できます。また、コンポーザブルが正しくカプセル化されているか、独立して動作するかを確認することをおすすめします。これにより、UI テストをより簡単かつ集中的に行うことができます。
これは、単体 UI テストのみを作成するということではありません。UI テストでは、UI の大きな部分にスコープ設定することも非常に重要です。
独自のコンテンツの設定後にアクティビティとリソースにアクセスする
多くの場合、composeTestRule.setContent
を使用してテスト対象のコンテンツを設定する必要があり、アクティビティ リソースにアクセスする必要もあります。たとえば、表示されるテキストが文字列リソースに一致することをアサートします。ただし、アクティビティがすでに setContent
を呼び出している場合、createAndroidComposeRule()
で作成したルールに対してまた呼び出すことはできません。
これを実現するための一般的なパターンは、空のアクティビティ(ComponentActivity
など)を使用して AndroidComposeTestRule
を作成することです。
class MyComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = exampleUiState, /*...*/)
}
}
val continueLabel = composeTestRule.activity.getString(R.string.next)
composeTestRule.onNodeWithText(continueLabel).performClick()
}
}
アプリの AndroidManifest.xml
ファイルに ComponentActivity
を追加する必要があるのでご注意ください。そのためには、次の依存関係をモジュールに追加します。
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
カスタム セマンティクス プロパティ
テストに情報を公開するカスタム セマンティクス プロパティを作成できます。作成するには、新しい SemanticsPropertyKey
を定義し、SemanticsPropertyReceiver
を使用して利用可能にします。
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
そうすると、semantics
修飾子でこのプロパティを使用できるようになります。
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
テストでは、SemanticsMatcher.expectValue
を使用してプロパティの値をアサートできます。
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
状態の復元を検証する
アクティビティまたはプロセスを再作成する場合、Compose 要素の状態が正しく復元されていることを検証してください。StateRestorationTester
クラスを使用すると、アクティビティの再作成に依存せずに、こうしたチェックを行えます。
このクラスを使用すると、コンポーザブルの再作成をシミュレートできます。特に、rememberSaveable
の実装を検証するのに便利です。
class MyStateRestorationTests {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun onRecreation_stateIsRestored() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent { MainScreen() }
// TODO: Run actions that modify the state
// Trigger a recreation
restorationTester.emulateSavedInstanceStateRestore()
// TODO: Verify that state has been correctly restored.
}
}
デバッグ
テストで問題を解決する主な方法は、セマンティクス ツリーを確認することです。テストの任意の時点で composeTestRule.onRoot().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'
こうしたログには、バグを追跡するための貴重な情報が含まれています。
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()))
}
UiAutomator との相互運用
デフォルトでは、使い勝手のよい記述子(表示されるテキストやコンテンツの説明など)によってのみ、UiAutomator からコンポーザブルにアクセスできます。Modifier.testTag
を使用する任意のコンポーザブルにアクセスするには、そのコンポーザブルのサブツリーに対してセマンティック プロパティ testTagAsResourceId
を有効にする必要があります。この動作を有効にすると、スクロール可能なコンポーザブル(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
詳細
詳細については、Jetpack Compose テスト Codelab をご覧ください。