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 状態でのみ機能します)。
アニメーション テストを最適化する
高忠実度のアニメーションをテストする場合、中間 UI 状態をアサートするために、自動進行を無効にしてフレームを手動でステップ実行する必要があることがよくあります。これらの特定のフレーム単位のループでは、runWithoutImplicitWait メソッドを使用してアサーションを実行します。標準ノードクエリ(onNodeWithTag や fetchSemanticsNode など)は、手動でクロックを制御している場合は冗長な暗黙的な同期をトリガーするため、それらをバイパスするとテストの実行時間が大幅に短縮されます。
使用上のガイドライン
- 手動クロック管理:
mainClock.autoAdvanceがfalseに設定され、UI が現在のフレームの既知の安定した状態にある場合に、この API を使用します。 - UI スレッドの実行: UI ツリーの安定性を確保するため、
runOnUiThreadなどを使用して、UI スレッドでrunWithoutImplicitWaitを呼び出します。UI スレッドから実行すると、テストが競合状態や古い状態の読み取りにさらされます。 - 読み取り専用アサーション: ブロックには読み取り専用アサーションのみが含まれている必要があります。状態を変更するアクションは、このブロックの外側で実行する必要があります。
例
@Test fun runWithoutImplicitWaitSample() = runComposeUiTest { setContent { MainScreen() } mainClock.autoAdvance = false // Trigger an animation onNodeWithText("Start Animation").performClick() // Step through the animation frame-by-frame while (hasPendingWork()) { mainClock.advanceTimeByFrame() waitForIdle() runOnUiThread { // Suppress implicit synchronization inside this block to avoid redundant // waits on each node query, making the frame assertions execute much faster. runWithoutImplicitWait { val box1 = onNodeWithTag("Box1").fetchSemanticsNode() val box2 = onNodeWithTag("Box2").fetchSemanticsNode() val box3 = onNodeWithTag("Box3").fetchSemanticsNode() // Assert the exact intermediate state of all three properties for this frame assert(box1.boundsInRoot.right <= box2.boundsInRoot.left) assert(box2.boundsInRoot.right <= box3.boundsInRoot.left) } } } }
メインスレッドの同期
Compose テストでメインスレッドの同期がサポートされるようになり、メインスレッドから waitForIdle(および Compose UI アクションとアサーション)を直接安全に呼び出すことができるようになりました。
以前の Compose テストでは、2 つのスレッドモデルが厳密に適用されていました。テスト実行はバックグラウンドのテストスレッドで行われ、UI の更新はメインスレッドで行われていました。メインスレッド(たとえば runOnUiThread ブロック内)から waitForIdle や runOnIdle などの同期メソッドを呼び出すと、IllegalStateException がスローされます。これは、メインスレッドの同期を防ぐために、フレームワークが厳格なスレッド チェックを適用したためです。
メインスレッドの同期が有効になっている場合、Compose テスト フレームワークは、メインスレッドでブロッキング呼び出しが行われた場合でも、クロックを進めて保留中の作業を処理できるようになりました。
メインスレッドの同期を使用する状況
純粋な Compose テストでは、テストをバックグラウンド スレッドで実行することが標準ですが、特定のシナリオではメインスレッドの同期が非常に有利です。
- 複雑な View の相互運用性: Compose と以前の Android View の両方を含むハイブリッド UI をテストする場合、View の操作にはメインスレッドでの実行が必要になることがよくあります。スレッド コンテキストを頻繁に切り替えることなく、View を操作して Compose ノードを順番にアサートできるようになりました。
- 同期状態のミューテーション: アーキテクチャが厳密にメインスレッドにバインドされた状態ホルダーに依存している場合、メインスレッドを離れることなく、状態をミューテーションして Compose UI が落ち着くのをすぐに待つことができます。
- カスタム テストランナー: カスタム テスト インフラストラクチャを構築している場合や、テストランナーがメインスレッドで実行される環境を利用している場合、Compose テストはバックグラウンド スレッドの委任を必要とせずにクリーンに実行されるようになりました。
例
これまで、メインスレッドでの同期は厳しく禁止されていたため、デベロッパーはバックグラウンド テストランナー スレッドと UI スレッドの間を往復する必要があり、テストが分断されていました。
@Test fun testBidirectionalInteropUIUpdates_old() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() } // Jump to Test Thread to verify state settles inside compose composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() // Jump back to Main Thread to verify target view state settles scenario.onFragment { fragment -> assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
メインスレッドの同期が有効になっている場合、Compose と View の階層のアサーションを同じブロックで実行できます。
@Test fun testBidirectionalInteropUIUpdates_new() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
条件を待機
データ読み込みや Android の測定または描画(つまり、Compose の外部の測定または描画)などの外部の処理に依存する条件では、waitUntil() のようなより一般的なコンセプトを採用する必要があります。
composeTestRule.waitUntil(timeoutMs) { condition }
waitUntil ヘルパーも使用できます。
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
参考情報
- Android でアプリをテストする: Android テストのメイン ランディング ページで、テストの基礎と手法についてより広範な視点から説明しています。
- テストの基礎: Android アプリのテストの背景にある基本概念について詳しく学べます。
- ローカルテスト: 一部のテストは、自分のワークステーションでローカルに実行できます。
- インストルメンテーション テスト: インストルメンテーション テストも実行することをおすすめします。つまり、デバイス上で直接実行されるテストです。
- 継続的インテグレーション: 継続的インテグレーションを使用すると、テストをデプロイ パイプラインに統合できます。
- さまざまな画面サイズをテストする: ユーザーが利用できるデバイスは多種多様であるため、さまざまな画面サイズでテストする必要があります。
- Espresso: ビューベースの UI を対象としていますが、Espresso の知識は Compose テストのいくつかの側面で役立ちます。