Synchroniser vos tests

Les tests Compose sont synchronisés par défaut avec votre interface utilisateur. Lorsque vous appelez une assertion ou une action avec le ComposeTestRule, le test est préalablement synchronisé, en attendant que l'arborescence de l'interface utilisateur soit inactive.

En général, aucune action n'est requise de votre part. Cependant, il existe des situations à connaître.

Lorsqu'un test est synchronisé, votre application Compose est avancée dans le temps à l'aide d'une horloge virtuelle. Les tests Compose ne s'exécutent donc pas en temps réel et peuvent être aussi rapides que possible.

Toutefois, si vous n'utilisez pas les méthodes de synchronisation, aucune recomposition n'est effectuée et l'interface utilisateur semble être interrompue.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

Notez que cette exigence ne s'applique qu'aux hiérarchies Compose, et non au reste de l'application.

Désactiver la synchronisation automatique

Lorsque vous appelez une assertion ou une action via ComposeTestRule, par exemple assertExists(), votre test est synchronisé avec l'interface utilisateur de Compose. Dans certains cas, vous pouvez arrêter cette synchronisation et contrôler vous-même l'horloge. Par exemple, vous pouvez contrôler le moment auquel effectuer des captures d'écran précises d'une animation quand l'interface utilisateur est occupée. Pour désactiver la synchronisation automatique, définissez la propriété autoAdvance de la mainClock sur false :

composeTestRule.mainClock.autoAdvance = false

En général, vous avancerez vous-même le temps. Vous pouvez faire défiler exactement une image avec advanceTimeByFrame() ou une durée spécifique avec advanceTimeBy() :

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Ressources inactives

Compose peut synchroniser les tests et l'interface utilisateur afin que chaque action et assertion soit effectuée dans un état inactif, en attendant ou en avançant l'horloge selon le besoin. Cependant, certaines opérations asynchrones dont les résultats affectent l'état de l'interface utilisateur peuvent être exécutées en arrière-plan sans que le test ne les prenne en compte.

Créez et enregistrez ces ressources inactives dans votre test afin de les prendre en compte lorsque vous décidez si l'application testée est occupée ou inactive. Aucune action n'est requise de votre part, sauf si vous devez enregistrer des ressources d'inactivité supplémentaires, par exemple si vous exécutez une tâche en arrière-plan qui n'est pas synchronisée avec Espresso ou Compose.

Cette API est très semblable aux ressources d'inactivité d'Espresso, qui indiquent si le sujet testé est inactif ou occupé. Utilisez la règle de test Compose pour enregistrer la mise en œuvre de IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Synchronisation manuelle

Dans certains cas, vous devez synchroniser l'interface utilisateur de Compose avec d'autres parties de votre test ou avec l'application que vous testez.

La fonction waitForIdle() attend que Compose soit inactif, mais la fonction dépend de la propriété autoAdvance :

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

Notez que dans les deux cas, waitForIdle() attend également les transferts de dessin et de mise en page en attente.

Vous pouvez aussi avancer le temps jusqu'à ce qu'une certaine condition soit remplie avec advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Notez que la condition précisée doit vérifier l'état pouvant être affecté par cette horloge (elle ne fonctionne qu'avec l'état Compose).

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 images pour affirmer les états intermédiaires de l'interface utilisateur. Pour ces boucles image par image spécifiques, utilisez la runWithoutImplicitWait méthode pour exécuter vos assertions. Les requêtes de nœud standard (telles que onNodeWithTag ou fetchSemanticsNode) déclenchent des synchronisations implicites qui sont redondantes lorsque vous contrôlez manuellement l'horloge. Les contourner accélère donc considérablement les durées 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'interface utilisateur est dans un état stable connu pour l'image actuelle.
  • Exécution du thread UI : pour garantir la stabilité de l'arborescence de l'interface utilisateur, appelez runWithoutImplicitWait sur le thread UI, par exemple avec runOnUiThread. L'exécution en dehors du thread UI expose votre test à des conditions de concurrence et à des lectures d'état obsolètes.
  • Assertions en lecture seule : le bloc doit contenir strictement 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)
            }
        }
    }
}

Synchronisation du thread principal

Les tests Compose sont désormais compatibles avec la synchronisation du thread principal, ce qui vous permet d'appeler en toute sécurité waitForIdle (et, par extension, les actions et assertions de l'interface utilisateur de Compose) directement à partir du thread principal.

Auparavant, les tests Compose appliquaient strictement un modèle à deux threads : l'exécution des tests se produisait sur un thread de test en arrière-plan, tandis que les mises à jour de l'interface utilisateur se produisaient sur le thread principal. L'appel de méthodes de synchronisation telles que waitForIdle ou runOnIdle à partir du thread principal (par exemple, dans un bloc runOnUiThread) générait une IllegalStateException, car le framework appliquait des vérifications strictes des threads pour empêcher la synchronisation du thread principal.

Lorsque la synchronisation du thread principal est activée, le framework de test Compose peut désormais avancer l'horloge et traiter le travail en attente, même lorsque des appels bloquants sont effectués sur le thread principal.

Quand utiliser la synchronisation du thread principal ?

Bien que le maintien des tests sur le thread en arrière-plan reste la norme pour les tests Compose purs, la synchronisation du thread principal est très avantageuse dans quelques cas spécifiques :

  • Interopérabilité complexe des vues : lors du test d'interfaces utilisateur hybrides contenant à la fois des vues Compose et Android héritées, la manipulation des vues nécessite souvent une exécution sur le thread principal. Vous pouvez désormais interagir avec les vues et effectuer des assertions sur les nœuds Compose de manière séquentielle sans avoir à changer constamment de contexte de thread.
  • Mutations d'état synchrones : si votre architecture repose sur des détenteurs d'état strictement liés au thread principal, vous pouvez désormais modifier l'état et attendre immédiatement que l'interface utilisateur de Compose se stabilise sans quitter le thread principal.
  • Exécuteurs de tests personnalisés : si vous créez une infrastructure de test personnalisée ou si vous utilisez des environnements dans lesquels l'exécuteur de tests s'exécute de manière inhérente sur le thread principal, les tests Compose s'exécutent désormais de manière propre sans nécessiter de délégation de thread en arrière-plan.

Exemple

Historiquement, comme la synchronisation était strictement interdite sur le thread UI principal, les développeurs devaient faire des allers-retours entre le thread de l'exécuteur de tests en arrière-plan et le thread UI, ce qui entraînait des tests disjoints :

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Lorsque la synchronisation du thread principal est activée, les assertions pour les hiérarchies Compose et View peuvent être exécutées dans le même bloc :

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Attendre des conditions

Toute condition qui dépend d'un travail externe, tel que le chargement de données ou la mesure ou le dessin d'Android (c'est-à-dire, une mesure ou un dessin externe à Compose), doit utiliser un concept plus général comme waitUntil() :

composeTestRule.waitUntil(timeoutMs) { condition }

Vous pouvez également utiliser l'un des waitUntil assistants :

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Autres ressources

  • Tester des applications sur Android : la page de destination principale des tests Android offre une vue plus large des principes de base et des techniques de test.
  • Principes fondamentaux des tests : découvrez les concepts de base des tests d'une application Android. Découvrez-en davantage sur les concepts de base des tests d'une application Android.
  • Tests locaux: vous pouvez exécuter certains tests localement, sur votre propre station de travail.
  • Tests d'instrumentation: il est recommandé d'exécuter également des tests d'instrumentation. Il s'agit de tests qui s'exécutent directement sur l'appareil.
  • Intégration continue: L'intégration continue vous permet d'intégrer vos tests à votre pipeline de déploiement.
  • Tester différentes tailles d'écran : étant donné le nombre d'appareils disponibles pour les utilisateurs, vous devez tester différentes tailles d'écran.
  • Espresso : bien qu'il soit destiné aux interfaces utilisateur basées sur les vues, Espresso peut être utile pour certains aspects des tests Compose.