测试动画

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()
}

优化动画测试

测试高保真动画时,您通常需要停用自动前进功能,并手动逐帧浏览,以断言中间界面状态。对于这些特定的逐帧循环,请使用 runWithoutImplicitWait 方法来执行断言。当您手动控制时钟时,标准节点查询(例如 onNodeWithTagfetchSemanticsNode)会触发冗余的隐式同步,因此绕过这些查询可以显著缩短测试运行时长。

使用指南

  • 手动时钟管理:当 mainClock.autoAdvance 设置为 false 且界面处于当前帧的已知稳定状态时,请使用此 API。
  • 界面线程执行:为确保界面树的稳定性,请在界面线程上调用 runWithoutImplicitWait,例如使用 runOnUiThread。在界面线程之外运行它会使您的测试面临竞态条件和过时的状态读取。
  • 只读断言:相应代码块应严格包含只读断言。任何会改变状态的操作都应在此代码块之外执行。

示例

@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)
            }
        }
    }
}