Как тестировать анимацию

Compose offers 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, чтобы выполнить утверждения. Стандартные запросы узлов (например, onNodeWithTag или fetchSemanticsNode) запускают неявные синхронизации, которые избыточны, когда вы вручную управляете часами, поэтому их обход значительно ускоряет выполнение тестов.

Рекомендации по использованию

  • Управление часами вручную. Используйте этот API, когда для параметра mainClock.autoAdvance задано значение false и интерфейс находится в известном стабильном состоянии для текущего кадра.
  • Выполнение потока UI. Чтобы обеспечить стабильность дерева UI, вызовите runWithoutImplicitWait в потоке UI, например с помощью 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)
            }
        }
    }
}