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

Compose の ComposeTestRule を使用すると、決定論的な方法で、テストクロックを完全に制御しながらアニメーションのテストを作成できます。これにより、アニメーションの中間値を検証できます。また、テストでは、実際の持続時間よりも短い時間でアニメーションを実行できます。

ComposeTestRule は、テストクロックを mainClock として公開します。テストコードのクロックを制御するには、autoAdvance プロパティを false に設定します。テストするアニメーションを開始したら、クロックを advanceTimeBy で進めることができます。

advanceTimeBy は、指定された持続時間分だけ正確にクロックを進めるのではなく、フレーム間隔の倍数に最も近い持続時間に切り上げることに注意してください。

@get:Rule
val rule = createComposeRule()

@Test
fun testAnimationWithClock() {
    // Pause animations
    rule.mainClock.autoAdvance = false
    var enabled by mutableStateOf(false)
    rule.setContent {
        val color by animateColorAsState(
            targetValue = if (enabled) Color.Red else Color.Green,
            animationSpec = tween(durationMillis = 250)
        )
        Box(Modifier.size(64.dp).background(color))
    }

    // Initiate the animation.
    enabled = true

    // Let the animation proceed.
    rule.mainClock.advanceTimeBy(50L)

    // Compare the result with the image showing the expected result.
    // `assertAgainGolden` needs to be implemented in your code.
    rule.onRoot().captureToImage().assertAgainstGolden()
}

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

高忠実度のアニメーションをテストする場合、中間 UI 状態をアサートするために、自動進行を無効にしてフレームを手動でステップ実行する必要があることがよくあります。これらの特定のフレーム単位のループでは、runWithoutImplicitWait メソッドを使用してアサーションを実行します。標準ノードクエリ(onNodeWithTagfetchSemanticsNode など)は、手動でクロックを制御している場合は冗長な暗黙的な同期をトリガーするため、それらをバイパスするとテストの実行時間が大幅に短縮されます。

使用上のガイドライン

  • 手動クロック管理: mainClock.autoAdvancefalse に設定され、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)
            }
        }
    }
}