Testar animações

O Compose oferece ComposeTestRule, que permite escrever testes de animações de forma determinística com controle total sobre o relógio de teste. Isso permite verificar valores de animação intermediários. Além disso, a duração do teste pode ser menor do que a da animação.

A ComposeTestRule exibe o relógio de teste como mainClock. Você pode definir a propriedade autoAdvance como "false" no código de teste para controlar o relógio. Depois de iniciar a animação a ser testada, o relógio pode ser adiantado com advanceTimeBy.

Observe que o advanceTimeBy não adianta o relógio exatamente de acordo com a duração especificada. Em vez disso, a duração é arredondada para o valor mais próximo que corresponda a um multiplicador da duração do frame.

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

Otimizar testes de animação

Ao testar animações de alta fidelidade, muitas vezes é necessário desativar o avanço automático e percorrer os frames manualmente para declarar estados intermediários da interface. Para esses loops específicos frame a frame, use o runWithoutImplicitWait método para executar suas declarações. Consultas de nós padrão (como onNodeWithTag ou fetchSemanticsNode) acionam sincronizações implícitas que são redundantes quando você está controlando o relógio manualmente. Portanto, ignorá-las acelera significativamente os tempos de execução do teste.

diretrizes de uso

  • Gerenciamento manual do relógio: use essa API quando mainClock.autoAdvance estiver definido como false e a interface estiver em um estado conhecido e estável para o frame atual.
  • Execução da linha de execução da interface: para garantir a estabilidade da árvore de interface, chame runWithoutImplicitWait na linha de execução da interface, como com runOnUiThread. A execução fora da linha de execução da interface expõe seu teste a condições de corrida e leituras de estado obsoletas.
  • Declarações somente leitura: o bloco precisa conter declarações somente leitura. Todas as ações que modificam o estado precisam ser realizadas fora desse bloco.

Exemplo

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