テストを同期する

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 がまだビジー状態である時点で、アニメーションの正確なスクリーンショットを撮る時間を制御できます。自動同期を無効にするには、mainClockautoAdvance プロパティを false に設定します。

composeTestRule.mainClock.autoAdvance = false

この場合、通常は手動で時間を進めます。advanceTimeByFrame() を使用してフレームを正確に 1 つだけ進めたり、advanceTimeBy() を使用して進める時間を指定したりできます。

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

アイドル状態のリソース

Compose は、テストと UI を同期することにより、すべてのアクションとアサーションがアイドル状態で実行され、必要に応じてクロックを待機させるか進めるようにすることができます。ただし、結果が UI 状態に影響する一部の非同期オペレーションは、テストによって認識されていないときにバックグラウンドで実行される可能性があります。

このようなアイドリング リソース をテスト内で作成して登録すると、テスト対象のアプリがビジー状態かアイドル状態かを判断する際に、それらのリソースが考慮されます。Espresso または Compose と同期されないバックグラウンド ジョブを実行する場合など、追加のアイドリング リソースを登録する必要がない場合は、何もする必要はありません。

この API は Espresso のアイドリング リソースとよく似ており、テスト対象がアイドル状態かビジー状態かを示します。Compose テストルールを使用して、 IdlingResource の実装を登録します。

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 状態でのみ機能します)。

アニメーション テストを最適化する

When testing high-fidelity animations, you often need to disable auto-advance and manually step through frames to assert intermediate UI states. For these specific frame-by-frame loops, use the runWithoutImplicitWait method to execute your assertions. Standard node queries (like onNodeWithTag or fetchSemanticsNode) trigger implicit synchronizations that are redundant when you are manually controlling the clock, so bypassing them significantly speeds up your test runtimes.

Usage guidelines

  • Manual clock management: Use this API when mainClock.autoAdvance is set to false and the UI is in a known, stable state for the current frame.
  • UI thread execution: To ensure the stability of the UI tree, call runWithoutImplicitWait on the UI thread, such as with runOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads.
  • Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.

Example

@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 ブロック内で)waitForIdlerunOnIdle などの同期メソッドを呼び出すと、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 テストのいくつかの側面で役立ちます。