Tester les animations

Compose propose ComposeTestRule, qui vous permet d'écrire des tests pour les animations de manière déterministe en contrôlant totalement l'horloge de test. Cela vous permet de vérifier les valeurs d'animation intermédiaires. En outre, un test peut s'exécuter plus rapidement que la durée réelle de l'animation.

ComposeTestRule expose son horloge de test en tant que mainClock. Vous pouvez définir la propriété autoAdvance sur "false" pour contrôler l'horloge dans votre code de test. Après avoir lancé l'animation à tester, vous pouvez avancer l'horloge avec advanceTimeBy.

Avec advanceTimeBy, vous n'avancez pas exactement l'horloge selon la durée spécifiée. L'heure est arrondie à la durée la plus proche, qui est un multiplicateur de la durée de l'image.

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

Optimiser les tests d'animation

Lorsque vous testez des animations haute fidélité, vous devez souvent désactiver l'avance automatique et parcourir manuellement les frames pour affirmer les états intermédiaires de l'UI. Pour ces boucles spécifiques image par image, utilisez la méthode runWithoutImplicitWait pour exécuter vos assertions. Les requêtes de nœud standards (comme onNodeWithTag ou fetchSemanticsNode) déclenchent des synchronisations implicites qui sont redondantes lorsque vous contrôlez manuellement l'horloge. Les ignorer accélère donc considérablement la durée d'exécution de vos tests.

Consignes d'utilisation

  • Gestion manuelle de l'horloge : utilisez cette API lorsque mainClock.autoAdvance est défini sur false et que l'UI est dans un état stable connu pour le frame actuel.
  • Exécution du thread UI : pour assurer la stabilité de l'arborescence de l'UI, appelez runWithoutImplicitWait sur le thread UI, par exemple avec runOnUiThread. L'exécution en dehors du thread d'UI expose votre test à des conditions de concurrence et à des lectures d'état obsolètes.
  • Assertions en lecture seule : le bloc doit contenir uniquement des assertions en lecture seule. Toutes les actions qui modifient l'état doivent être effectuées en dehors de ce bloc.

Exemple

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