1. Introduction et configuration
Dans cet atelier de programmation, vous apprendrez à tester des interfaces utilisateur créées avec Jetpack Compose. Vous rédigerez vos premiers tests en apprenant à effectuer des tests isolés et à déboguer des tests, et en découvrant les arborescences sémantiques et la synchronisation.
Ce dont vous avez besoin
- Dernière version d'Android Studio
- Connaissance de Kotlin
- Connaissances de base de Compose (annotation
@Composable
, entre autres) - Connaissances de base des modificateurs
- Facultatif : Il peut être utile de suivre l'atelier de programmation sur les principes de base de Jetpack Compose avant cet atelier de programmation.
Consulter le code pour cet atelier de programmation (Rally)
Vous allez utiliser l'étude Rally Material comme base de cet atelier de programmation. Vous la trouverez dans le dépôt GitHub android-compose-codelabs. Pour cloner le code, exécutez la commande suivante :
git clone https://github.com/android/codelab-android-compose.git
Une fois le téléchargement terminé, ouvrez le projet TestingCodelab
.
Vous pouvez également télécharger deux fichiers ZIP :
Ouvrez le dossier TestingCodelab qui contient une application appelée Rally.
Examiner la structure du projet
Les tests Compose sont des tests d'instrumentation. Cela signifie qu'ils nécessitent un appareil (appareil physique ou émulateur) sur lequel s'exécuter.
Rally contient déjà des tests d'interface utilisateur d'instrumentation. Vous les trouverez dans l'ensemble de sources androidTest :
Il s'agit du répertoire dans lequel vous allez placer les nouveaux tests. N'hésitez pas à examiner le fichier AnimatingCircleTests.kt
pour découvrir à quoi ressemble un test Compose.
Rally est déjà configuré. Pour activer les tests Compose dans un nouveau projet, vous avez simplement besoin des dépendances de test du fichier build.gradle
du module concerné, à savoir :
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$rootProject.composeVersion"
N'hésitez pas à exécuter l'application et à vous familiariser avec celle-ci.
2. Que faut-il tester ?
Concentrons-nous maintenant sur la barre d'onglets de Rally, qui contient une ligne d'onglets : "Overview" (Aperçu), "Accounts" (Comptes) et "Bills" (Factures). Elle se présente ainsi :
Dans cet atelier de programmation, vous testerez l'interface utilisateur de la barre.
Cela peut avoir plusieurs significations :
- Vérifier que les onglets affichent l'icône et le texte souhaités
- Vérifier que l'animation correspond aux spécifications
- Vérifier que les événements de navigation déclenchés sont corrects
- Tester l'emplacement et la distance des éléments de l'interface utilisateur dans différents états
- Faire une capture d'écran de la barre et la comparer à une capture d'écran précédente
Il n'existe aucune règle précise concernant la quantité ou le mode de test d'un composant. Vous pourriez faire tout cela ! Dans cet atelier de programmation, vous allez tester la logique d'état pour vous assurer qu'elle est correcte, en vérifiant les éléments suivants :
- Un onglet n'affiche son libellé que lorsqu'il est sélectionné.
- L'écran actif définit l'onglet sélectionné.
3. Créer un test d'interface utilisateur simple
Créer le fichier TopAppBarTest
Créez un fichier dans le même dossier que AnimatingCircleTests.kt
(app/src/androidTest/com/example/compose/rally
) et appelez-le TopAppBarTest.kt
.
Compose est fourni avec une règle ComposeTestRule
que vous pouvez obtenir en appelant createComposeRule()
. Cette règle vous permet de définir le contenu Compose testé et d'interagir avec.
Ajouter la règle ComposeTestRule
package com.example.compose.rally
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
class TopAppBarTest {
@get:Rule
val composeTestRule = createComposeRule()
// TODO: Add tests
}
Effectuer des tests isolés
Dans un test Compose, nous pouvons lancer l'activité principale de l'application de la même manière que dans Android View, en utilisant Espresso, par exemple. Vous pouvez le faire avec createAndroidComposeRule
.
// Don't copy this over
@get:Rule
val composeTestRule = createAndroidComposeRule(RallyActivity::class.java)
Toutefois, Compose nous permet de simplifier considérablement les choses en testant un composant de façon isolée. Vous pouvez choisir le contenu de l'interface utilisateur Compose à utiliser dans le test. Pour ce faire, utilisez la méthode setContent
de la règle ComposeTestRule
, que vous pouvez appeler n'importe où (mais une seule fois).
// Don't copy this over
class TopAppBarTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myTest() {
composeTestRule.setContent {
Text("You can set any Compose content!")
}
}
}
Nous voulons tester la TopAppBar. Concentrons-nous dessus. Appelez la barre RallyTopAppBar
dans setContent
, puis laissez Android Studio renseigner les noms des paramètres.
import androidx.compose.ui.test.junit4.createComposeRule
import com.example.compose.rally.ui.components.RallyTopAppBar
import org.junit.Rule
import org.junit.Test
class TopAppBarTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun rallyTopAppBarTest() {
composeTestRule.setContent {
RallyTopAppBar(
allScreens = ,
onTabSelected = { /*TODO*/ },
currentScreen =
)
}
}
}
Importance d'un composable testable
La barre RallyTopAppBar
utilise trois paramètres faciles à fournir pour que nous puissions transmettre les données fictives que nous contrôlons. Exemple :
@Test
fun rallyTopAppBarTest() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
Thread.sleep(5000)
}
Nous ajoutons également sleep()
pour vous permettre de voir ce qui se passe. Effectuez un clic droit sur rallyTopAppBarTest
, puis cliquez sur "Run rallyTopAppBarTest()…" (Exécuter rallyTopAppBarTest()).
La barre d'application supérieure s'affiche (pendant 5 secondes), mais elle n'a pas l'apparence attendue : son thème est clair.
La raison est la suivante : la barre est conçue à l'aide des composants Material, qui s'attendent à être compris dans un MaterialTheme. Sinon, ils se rabattent sur les couleurs des styles de "référence".
Les paramètres par défaut de MaterialTheme
sont corrects afin d'éviter les plantages. Comme nous n'allons pas tester le thème ni effectuer de captures d'écran, nous pouvons l'omettre et utiliser son thème clair par défaut. N'hésitez pas à encapsuler RallyTopAppBar
avec RallyTheme
pour le corriger.
Vérifier que l'onglet est sélectionné
La recherche des éléments d'interface utilisateur, la vérification de leurs propriétés et l'exécution des actions s'effectuent via la règle de test, selon le modèle suivant :
composeTestRule{.finder}{.assertion}{.action}
Au cours de ce test, vous allez rechercher le mot "Accounts" (Comptes) pour vérifier que le libellé de l'onglet sélectionné est affiché.
Pour connaître les outils à votre disposition, vous pouvez utiliser l'aide-mémoire pour les tests Compose ou la documentation de référence sur les packages de test. Recherchez des outils de recherche et des assertions qui pourraient vous aider. Exemple : onNodeWithText
, onNodeWithContentDescription
, isSelected
, hasContentDescription
, assertIsSelected
…
Chaque onglet comporte une description de contenu différente :
- Aperçu
- Comptes
- Factures
Sachant cela, remplacez Thread.sleep(5000)
par une instruction qui recherche une description de contenu et déclare qu'elle existe :
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.onNodeWithContentDescription
...
@Test
fun rallyTopAppBarTest_currentTabSelected() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule
.onNodeWithContentDescription(RallyScreen.Accounts.name)
.assertIsSelected()
}
Exécutez à nouveau le test. Vous devriez voir un test vert :
Félicitations ! Vous avez rédigé votre premier test Compose. Vous avez appris à effectuer des tests isolés et à utiliser des outils de recherche et des assertions.
Bien que simple, cette méthode nécessitait certaines connaissances préalables du composant (les descriptions de contenu et la propriété sélectionnée). Vous apprendrez à inspecter les propriétés disponibles à l'étape suivante.
4. Déboguer les tests
Au cours de cette étape, vous allez vérifier que le libellé de l'onglet actif est affiché en majuscules.
Une solution possible serait d'essayer de trouver le texte et de confirmer qu'il existe :
import androidx.compose.ui.test.onNodeWithText
...
@Test
fun rallyTopAppBarTest_currentLabelExists() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule
.onNodeWithText(RallyScreen.Accounts.name.uppercase())
.assertExists()
}
Cependant, lorsque vous exécutez le test, il échoue 😱
Au cours de cette étape, vous découvrirez comment résoudre ce problème à l'aide de l'arborescence sémantique.
Arborescence sémantique
Les tests Compose utilisent une structure appelée arborescence sémantique pour rechercher des éléments à l'écran et lire leurs propriétés. Cette structure est également utilisée par les services d'accessibilité, car elle est destinée à être lue par un service tel que TalkBack.
Vous pouvez imprimer l'arborescence sémantique à l'aide de la fonction printToLog
sur un nœud. Ajoutez une nouvelle ligne au test :
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
...
fun rallyTopAppBarTest_currentLabelExists() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule.onRoot().printToLog("currentLabelExists")
composeTestRule
.onNodeWithText(RallyScreen.Accounts.name.uppercase())
.assertExists() // Still fails
}
Exécutez à présent le test et consultez le Logcat dans Android Studio (vous pouvez rechercher currentLabelExists
).
...com.example.compose.rally D/currentLabelExists: printToLog:
Printing with useUnmergedTree = 'false'
Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
|-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
[SelectableGroup]
MergeDescendants = 'true'
|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
| Role = 'Tab'
| Selected = 'false'
| StateDescription = 'Not selected'
| ContentDescription = 'Overview'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
|-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
| Role = 'Tab'
| Selected = 'true'
| StateDescription = 'Selected'
| ContentDescription = 'Accounts'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
|-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
Role = 'Tab'
Selected = 'false'
StateDescription = 'Not selected'
ContentDescription = 'Bills'
Actions = [OnClick]
MergeDescendants = 'true'
ClearAndSetSemantics = 'true'
En examinant l'arborescence sémantique, vous pouvez voir qu'il existe un SelectableGroup
avec trois éléments enfants, qui correspondent aux onglets de la barre d'application supérieure. Il s'avère qu'aucune propriété text
n'est associée à la valeur "ACCOUNTS". C'est pourquoi le test échoue. Toutefois, vous trouverez une description du contenu pour chaque onglet. Vous pouvez vérifier comment cette propriété est définie dans le composable RallyTab
dans RallyTopAppBar.kt
:
private fun RallyTab(text: String...)
...
Modifier
.clearAndSetSemantics { contentDescription = text }
Ce modificateur efface les propriétés des descendants et définit sa propre description de contenu. C'est pourquoi vous voyez "Accounts" et non "ACCOUNTS".
Remplacez onNodeWithText
par onNodeWithContentDescription
et exécutez de nouveau le test :
fun rallyTopAppBarTest_currentLabelExists() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule
.onNodeWithContentDescription(RallyScreen.Accounts.name)
.assertExists()
}
Félicitations ! Vous avez corrigé le test et vous avez découvert la règle ComposeTestRule
, les tests isolés, les outils de recherche, les assertions et le débogage avec l'arborescence sémantique.
Malheureusement, ce test n'est pas très utile. Si vous examinez attentivement l'arborescence sémantique, les descriptions de contenu des trois onglets s'affichent, que l'onglet soit sélectionné ou non. Il faut aller plus loin !
5. Arborescences sémantiques fusionnées et non fusionnées
L'arborescence sémantique a pour objectif d'être aussi compacte que possible et de n'afficher que les informations pertinentes.
Par exemple, dans notre TopAppBar
, il n'est pas nécessaire que les icônes et les libellés soient différents. Examinez le nœud "Overview" (Aperçu) :
|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
| Role = 'Tab'
| Selected = 'false'
| StateDescription = 'Not selected'
| ContentDescription = 'Overview'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
Ce nœud comporte des propriétés (telles que Selected
et Role
) définies spécifiquement pour un composant selectable
et une description du contenu pour l'ensemble de l'onglet. Ces propriétés de haut niveau sont très utiles pour les tests simples. Les détails concernant l'icône ou le texte seraient redondants, ils n'apparaissent donc pas.
Compose expose automatiquement ces propriétés sémantiques dans certains composables tels que Text
. Vous pouvez également les personnaliser et les fusionner pour représenter un composant unique constitué d'un ou de plusieurs descendants. Par exemple, vous pouvez représenter un Button
contenant un composable Text
. La propriété MergeDescendants = 'true'
nous indique que ce nœud avait des descendants, qui ont été fusionnés avec lui. Dans les tests, nous devons souvent accéder à tous les nœuds.
Pour vérifier si le Text
situé dans l'onglet s'affiche ou non, nous pouvons interroger l'arborescence sémantique non fusionnée en transmettant useUnmergedTree = true
à l'outil de recherche onRoot
.
@Test
fun rallyTopAppBarTest_currentLabelExists() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
}
Le résultat dans Logcat est désormais légèrement plus long :
Printing with useUnmergedTree = 'true'
Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
|-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
[SelectableGroup]
MergeDescendants = 'true'
|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
| Role = 'Tab'
| Selected = 'false'
| StateDescription = 'Not selected'
| ContentDescription = 'Overview'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
|-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
| Role = 'Tab'
| Selected = 'true'
| StateDescription = 'Selected'
| ContentDescription = 'Accounts'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
| |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
| Text = 'ACCOUNTS'
| Actions = [GetTextLayoutResult]
|-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
Role = 'Tab'
Selected = 'false'
StateDescription = 'Not selected'
ContentDescription = 'Bills'
Actions = [OnClick]
MergeDescendants = 'true'
ClearAndSetSemantics = 'true'
Le nœud 3 n'a toujours pas de descendants :
|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
| Role = 'Tab'
| Selected = 'false'
| StateDescription = 'Not selected'
| ContentDescription = 'Overview'
| Actions = [OnClick]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
Toutefois, l'onglet sélectionné (nœud 6) en comporte un. Nous pouvons désormais voir la propriété "Text" (Texte) :
|-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
| Role = 'Tab'
| Selected = 'true'
| StateDescription = 'Selected'
| ContentDescription = 'Accounts'
| Actions = [OnClick]
| MergeDescendants = 'true'
| |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
| Text = 'ACCOUNTS'
| Actions = [GetTextLayoutResult]
Afin de vérifier que nous obtenons bien le comportement attendu, nous allons créer un outil de mise en correspondance qui trouve un nœud dont le texte est "ACCOUNTS" et dont le parent est un nœud avec la description de contenu "Accounts".
Consultez à nouveau l'aide-mémoire pour les tests Compose et essayez de trouver un moyen de créer cet outil de mise en correspondance. Notez que vous pouvez utiliser des opérateurs booléens tels que and
et or
avec les outils de mise en correspondance.
Tous les outils de recherche ont un paramètre appelé useUnmergedTree
. Définissez-le sur true
pour utiliser l'arborescence non fusionnée.
Essayez de rédiger le test sans regarder la solution !
Solution
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
...
@Test
fun rallyTopAppBarTest_currentLabelExists() {
val allScreens = RallyScreen.values().toList()
composeTestRule.setContent {
RallyTopAppBar(
allScreens = allScreens,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule
.onNode(
hasText(RallyScreen.Accounts.name.uppercase()) and
hasParent(
hasContentDescription(RallyScreen.Accounts.name)
),
useUnmergedTree = true
)
.assertExists()
}
Exécutez le test :
Félicitations ! Au cours de cette étape, vous avez découvert la fusion des propriétés, ainsi que les arborescences sémantiques fusionnées et non fusionnées.
6. Synchronisation
Tout test que vous rédigez doit être correctement synchronisé avec l'objet du test. Par exemple, lorsque vous utilisez un outil de recherche tel que onNodeWithText
, le test attend que l'application soit inactive avant d'interroger l'arborescence sémantique. Sans synchronisation, les tests pourraient rechercher des éléments avant qu'ils ne soient affichés ou attendre inutilement.
Nous utiliserons l'écran "Overview" (Aperçu) pour cette étape, qui a l'apparence suivante lorsque vous exécutez l'application :
Notez l'animation clignotante récurrente de la fiche "Alerts" (Alertes), attirant ainsi l'attention sur cet élément.
Créez une autre classe de test appelée OverviewScreenTest
et ajoutez le contenu suivant :
package com.example.compose.rally
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.example.compose.rally.ui.overview.OverviewBody
import org.junit.Rule
import org.junit.Test
class OverviewScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun overviewScreen_alertsDisplayed() {
composeTestRule.setContent {
OverviewBody()
}
composeTestRule
.onNodeWithText("Alerts")
.assertIsDisplayed()
}
}
Si vous exécutez ce test, vous remarquerez qu'il ne se termine jamais (il expire au bout de 30 secondes).
Le message d'erreur suivant s'affiche :
androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.android.ComposeIdlingResource@d075f91
Cela signifie simplement que Compose est occupé de manière permanente. Il n'y a donc aucun moyen de synchroniser l'application avec le test.
Vous avez peut-être déjà deviné que l'animation clignotante infinie est le problème ici. L'application n'étant jamais inactive, le test ne peut pas se poursuivre.
Examinons l'implémentation de l'animation infinie :
app/src/main/java/com/example/compose/rally/ui/overview/OverviewBody.kt
var currentTargetElevation by remember { mutableStateOf(1.dp) }
LaunchedEffect(Unit) {
// Start the animation
currentTargetElevation = 8.dp
}
val animatedElevation = animateDpAsState(
targetValue = currentTargetElevation,
animationSpec = tween(durationMillis = 500),
finishedListener = {
currentTargetElevation = if (currentTargetElevation > 4.dp) {
1.dp
} else {
8.dp
}
}
)
Card(elevation = animatedElevation.value) { ... }
Ce code attend qu'une animation se termine (finishedListener
), puis l'exécute à nouveau.
Pour résoudre ce test, vous pouvez désactiver les animations dans les options pour les développeurs. C'est l'un des modes de résolution les plus utilisés dans le monde de View
.
Dans Compose, les API Animation ont été conçues pour être testables. Vous pouvez donc résoudre le problème en utilisant l'API appropriée. Au lieu de redémarrer l'animation animateDpAsState
, nous pouvons utiliser des animations infinies.
Remplacez le code dans OverviewScreen
par l'API appropriée :
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.Dp
...
val infiniteElevationAnimation = rememberInfiniteTransition()
val animatedElevation: Dp by infiniteElevationAnimation.animateValue(
initialValue = 1.dp,
targetValue = 8.dp,
typeConverter = Dp.VectorConverter,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
)
)
Card(elevation = animatedElevation) {
Si vous exécutez le test, il fonctionnera désormais :
Félicitations ! Au cours de cette étape, vous avez découvert la synchronisation et l'impact des animations sur les tests.
7. Exercice facultatif
Au cours de cette étape, vous allez utiliser une action (voir l'aide-mémoire pour les tests) pour vérifier qu'un clic sur les différents onglets de la RallyTopAppBar
permet de modifier la sélection.
Conseils :
- Le champ d'application du test doit inclure l'état (qui appartient à
RallyApp
). - Vérifiez l'état, pas le comportement. Utilisez des assertions sur l'état de l'interface utilisateur au lieu de vous appuyer sur les objets appelés et le mode d'appel.
Aucune solution n'est proposée pour cet exercice.
8. Étapes suivantes
Félicitations ! Vous avez terminé l'atelier Tester dans Jetpack Compose. Vous disposez désormais des éléments de base pour créer une stratégie de test efficace pour vos interfaces utilisateur Compose.
Si vous souhaitez en savoir plus sur les fonctionnalités de test et sur Compose, consultez les ressources suivantes :
- La documentation sur les tests contient plus d'informations sur les outils de recherche, les assertions, les actions et les outils de mise en correspondance, ainsi que sur les mécanismes de synchronisation, la manipulation du temps, etc.
- Ajoutez l'aide-mémoire pour les tests à vos favoris.
- L'exemple Rally est fourni avec une classe de test de capture d'écran simple. Explorez le fichier
AnimatingCircleTests.kt
pour en savoir plus à son sujet. - Pour obtenir des conseils sur le test des applications Android, vous pouvez suivre ces trois ateliers de programmation :
- Testing Basics (Principes de base des tests)
- Dependency Injection and Test Doubles (Injection de dépendances et doubles de test)
- Survey of Testing Topics (Enquête portant sur les thèmes des tests)
- Le dépôt Compose samples sur GitHub contient plusieurs applications avec des tests de l'interface utilisateur.
- Le chemin d'accès Jetpack Compose présente une liste de ressources pour vous aider à démarrer avec Compose.
Bon test !