測試動畫

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.autoAdvance 設為 false,且 UI 處於目前影格的已知穩定狀態,請使用這個 API。
  • UI 執行緒執行:為確保 UI 樹狀結構穩定,請在 UI 執行緒上呼叫 runWithoutImplicitWait,例如使用 runOnUiThread。如果從 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)
            }
        }
    }
}