Les interfaces utilisateur ou les écrans de test permettent de vérifier que votre code Compose se comporte correctement et ainsi d'améliorer la qualité de votre application en identifiant les erreurs dès le début du processus de développement.
Compose fournit un ensemble d'API de test pour trouver des éléments, vérifier leurs attributs et effectuer des actions utilisateur. D'autres fonctionnalités avancées, telles que la manipulation du temps, sont également proposées.
Sémantique
Dans Compose, les tests de l'interface utilisateur utilisent la sémantique pour interagir avec la hiérarchie de l'interface utilisateur. Comme son nom l'indique, la sémantique donne du sens à une interface utilisateur. Dans ce contexte, un "élément d'interface utilisateur" (ou élément) peut prendre toutes les formes, d'un composable unique à un plein écran. L'arborescence sémantique est générée parallèlement à la hiérarchie de l'interface utilisateur et la décrit.
Image 1. Hiérarchie classique de l'UI avec son arborescence sémantique.
Le framework sémantique étant principalement utilisé pour l'accessibilité, les tests exploitent les informations exposées par la sémantique concernant la hiérarchie de l'interface utilisateur. Les développeurs décident de ce qu'ils exposent et en quelle quantité.
Image 2. Bouton type contenant une icône et du texte.
Par exemple, pour un bouton de ce type composé d'une icône et d'un élément de texte, l'arborescence sémantique par défaut ne contient que la mention "J'aime". En effet, certains composables, comme Text
, exposent déjà certaines propriétés à l'arborescence sémantique. Vous pouvez ajouter des propriétés à l'arborescence sémantique à l'aide d'un Modifier
.
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
Configuration
Cette section explique comment configurer votre module pour tester le code Compose.
Commencez par ajouter les dépendances suivantes au fichier build.gradle
du module contenant vos tests d'interface utilisateur :
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
Ce module inclut une ComposeTestRule
et une intégration pour Android appelée AndroidComposeTestRule
. Cette règle vous permet de configurer du contenu Compose ou d'accéder à l'activité. Voici à quoi ressemble un test d'interface utilisateur pour Compose :
// file: app/src/androidTest/java/com/package/MyComposeTest.kt
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = fakeUiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
API de test
Il existe trois façons principales d'interagir avec les éléments :
- Les outils de recherche permettent de sélectionner un ou plusieurs éléments (ou nœuds de l'arborescence sémantique) pour définir des assertions ou effectuer des actions par rapport à eux.
- Les assertions permettent de vérifier que les éléments existent ou qu'ils possèdent certains attributs.
- Les actions ajoutent des événements utilisateur simulés sur les éléments tels que des clics ou d'autres gestes.
Certaines de ces API acceptent un SemanticsMatcher
pour faire référence à un ou plusieurs nœuds de l'arborescence sémantique.
Outils de recherche
Vous pouvez utiliser onNode
et onAllNodes
pour sélectionner un ou plusieurs nœuds, mais vous pouvez également utiliser des paramètres de recherche définis pour les recherches les plus courantes, comme onNodeWithText
, onNodeWithContentDescription
, etc. Retrouvez-en la liste complète dans l'aide-mémoire pour les tests Compose.
Sélectionner un seul nœud
composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
.onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
Sélectionner plusieurs nœuds
composeTestRule
.onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
.onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
Utiliser l'arborescence non fusionnée
Certains nœuds fusionnent les informations sémantiques de leurs enfants. Par exemple, un bouton avec deux éléments de texte fusionne leurs étiquettes :
MyButton {
Text("Hello")
Text("World")
}
À partir d'un test, nous pouvons utiliser printToLog()
pour afficher l'arborescence sémantique :
composeTestRule.onRoot().printToLog("TAG")
Ce code imprime le résultat suivant :
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
Si vous devez faire correspondre un nœud de l'arborescence non fusionnée, vous pouvez définir useUnmergedTree
sur true
:
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
Ce code imprime le résultat suivant :
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = '[Hello]'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = '[World]'
Le paramètre useUnmergedTree
est disponible dans tous les outils de recherche. Il est par exemple utilisé dans un outil de recherche onNodeWithText
.
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
Assertions
Vérifiez les assertions en appelant assert()
sur le SemanticsNodeInteraction
renvoyé par un outil de recherche avec une ou plusieurs correspondances :
// Single matcher:
composeTestRule
.onNode(matcher)
.assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
composeTestRule
.onNode(matcher).assert(hasText("Button") or hasText("Button2"))
Vous pouvez également utiliser des fonctions définies pour les assertions les plus courantes, comme assertExists
, assertIsDisplayed
ou assertTextEquals
, etc. Retrouvez la liste complète dans l'aide-mémoire pour les tests Compose.
Il existe aussi des fonctions permettant de vérifier les assertions sur un ensemble de nœuds :
// Check number of matched nodes
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
Actions
Pour ajouter une action sur un nœud, appelez une fonction perform…()
:
composeTestRule.onNode(...).performClick()
Voici quelques exemples d'actions :
performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }
Retrouvez-en la liste complète dans l'aide-mémoire pour les tests Compose.
Outils de mise en correspondance
Cette section décrit certains des outils de mise en correspondance disponibles pour tester votre code Compose.
Outils de mise en correspondance hiérarchique
Les outils de mise en correspondance hiérarchique vous permettent de monter ou de descendre dans l'arborescence sémantique et d'effectuer une mise en correspondance simple.
fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher
Voici quelques exemples de mise en correspondance :
composeTestRule.onNode(hasParent(hasText("Button")))
.assertIsDisplayed()
Sélecteurs
Pour créer des tests, vous pouvez aussi utiliser des sélecteurs, qui permettent de les rendre plus lisibles.
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
Retrouvez-en la liste complète dans l'aide-mémoire pour les tests Compose.
Synchronisation
Les tests Compose sont synchronisés par défaut avec votre interface utilisateur. Lorsque vous appelez une assertion ou une action via 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()
}
Il est également important de noter 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.
Vous pouvez créer et enregistrer 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é. Vous 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.
waitForIdle
attend que Compose soit inactif, mais cela 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 dessins et de mises en page en attente.
Vous pouvez aussi avancer le temps jusqu'à ce qu'advanceTimeUntil()
remplisse une certaine condition.
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). Toute condition qui dépend d'une mesure ou d'un dessin d'Android (c'est-à-dire, d'une mesure ou d'un dessin externe à Compose) doit utiliser un concept plus général comme waitUntil()
:
composeTestRule.waitUntil(timeoutMs) { condition }
Schémas courants
Cette section décrit certaines approches courantes utilisées dans les tests Compose.
Effectuer un test isolé
ComposeTestRule
vous permet de lancer une activité affichant n'importe quel composable : votre application complète, un seul écran ou un petit élément. Il est également recommandé de vérifier que vos composables sont correctement encapsulés et fonctionnent de manière autonome afin de tester l'interface utilisateur plus facilement et plus précisément.
Cela ne signifie pas que vous devez uniquement créer des tests d'interface utilisateur. Il est également très important de tester la portée de plus grandes parties de votre interface utilisateur.
Accédez à l'activité et aux ressources après avoir défini votre propre contenu
Vous devez souvent configurer le contenu testé à l'aide de composeTestRule.setContent
et accéder aux ressources d'activité, par exemple pour affirmer qu'un texte affiché correspond à une ressource de chaîne. Toutefois, vous ne pouvez pas appeler setContent
sur une règle créée avec createAndroidComposeRule()
si l'activité l'appelle déjà.
Pour ce faire, il est courant de créer une AndroidComposeTestRule
à l'aide d'une activité vide (comme ComponentActivity
).
class MyComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = exampleUiState, /*...*/)
}
}
val continueLabel = composeTestRule.activity.getString(R.string.next)
composeTestRule.onNodeWithText(continueLabel).performClick()
}
}
Notez que ComponentActivity
doit être ajouté au fichier AndroidManifest.xml
de votre application. Pour ce faire, ajoutez la dépendance suivante à votre module :
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
Propriétés de la sémantique personnalisée
Vous pouvez créer des propriétés sémantiques personnalisées pour soumettre des informations aux tests. Pour ce faire, définissez une nouvelle SemanticsPropertyKey
et rendez-la disponible à l'aide de SemanticsPropertyReceiver
.
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
Vous pouvez maintenant utiliser cette propriété à l'aide du modificateur semantics
:
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
À partir des tests, vous pouvez utiliser SemanticsMatcher.expectValue
pour réclamer la valeur de la propriété :
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
Vérifier la restauration d'état
Vérifiez que l'état de vos éléments Compose est correctement restauré lorsque l'activité ou le processus est recréé. Il est possible d'effectuer cette vérification sans dépendre de la recréation d'activité avec la classe StateRestorationTester
.
Cette classe vous permet de simuler la recréation d'un composable. Elle s'avère particulièrement utile pour vérifier la mise en œuvre de rememberSaveable
.
class MyStateRestorationTests {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun onRecreation_stateIsRestored() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent { MainScreen() }
// TODO: Run actions that modify the state
// Trigger a recreation
restorationTester.emulateSavedInstanceStateRestore()
// TODO: Verify that state has been correctly restored.
}
}
Débogage
La principale façon de résoudre les problèmes de vos tests consiste à examiner l'arborescence sémantique.
Vous pouvez imprimer l'arborescence en appelant composeTestRule.onRoot().printToLog()
à tout moment de votre test. Cette fonction imprime un journal de la manière suivante :
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = 'Hi'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = 'There'
Ces journaux contiennent des informations utiles pour identifier les bugs.
Interopérabilité avec Espresso
Dans une application hybride, vous pouvez trouver des composants Compose dans des hiérarchies de vues et des vues dans des composables Compose (via le composable AndroidView
).
Aucune étape particulière n'est requise pour correspondre à chaque type. Vous faites correspondre les vues via l'onView
d'Espresso et les éléments Compose via la ComposeTestRule
.
@Test
fun androidViewInteropTest() {
// Check the initial state of a TextView that depends on a Compose state:
Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
// Click on the Compose button that changes the state
composeTestRule.onNodeWithText("Click here").performClick()
// Check the new value
Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
Interopérabilité avec UiAutomator
Par défaut, les composables ne sont accessibles depuis UiAutomator que par leurs descripteurs pratiques (texte affiché, description du contenu, etc.). Si vous souhaitez accéder à n'importe quel composable qui utilise Modifier.testTag
, vous devez activer la propriété sémantique testTagAsResourceId
pour cette sous-arborescence de composables.
Ce comportement est utile pour les composables qui ne possèdent pas d'autre identifiant unique, comme les composables à faire défiler (par exemple, LazyColumn
).
Vous ne pouvez l'activer qu'une seule fois au sommet de la hiérarchie de vos composables afin de vous assurer que tous les composables imbriqués avec Modifier.testTag
sont accessibles depuis UiAutomator.
Scaffold(
// Enables for all composables in the hierarchy.
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
){
// Modifier.testTag is accessible from UiAutomator for composables nested here.
LazyColumn(
modifier = Modifier.testTag("myLazyColumn")
){
// content
}
}
Tout composable avec Modifier.testTag(tag)
est accessible à l'aide de By.res(resourceName)
avec le même tag
que resourceName
.
val device = UiDevice.getInstance(getInstrumentation())
val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn
En savoir plus
Pour en savoir plus, consultez l'atelier de programmation Jetpack Compose.