Les tests automatisés
Introduction
Tester le code d’une application est essentiel pour garantir sa qualité et sa maintenabilité. C’est aussi un très bon moyen de partager les connaissances autour d’un projet. Avoir du code permet de faire évoluer et de refactoriser une application en toute sérénité. Un développeur ou une développeuse qui, lors d’une session de refactorisation, a juste à rejouer les tests de son application pour s’assurer que ses changements n’ont rien cassé est sans doute une développeuse heureuse ! Heureusement, Jetpack Compose a pour but de rendre tous les développeurs et développeuses heureux, en particulier grâce à la simplicité avec laquelle il est possible de tester une interface que ce soit à l’échelle d’un écran ou celle d’un composant.
Installation
Pour utiliser l’API de test de Jetpack Compose, il faut ajouter dans le fichier build.gradle du module concerné les dépendances suivantes :
def composeBom = platform("androidx.compose:compose-
bom:$compose_bom_version")
androidTestImplementation composeBom
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
La définition du BoM composeBom est donnée ici à titre indicatif. Elle n’est normalement pas nécessaire puisqu’il a déjà été défini lors de l’ajout de Jetpack Compose au projet. C’est ce que nous avons fait au début du chapitre Les bases de Compose UI.
Différents types de tests
Avant de découvrir comment tester une interface avec Jetpack Compose, faisons un point sur les différents moyens de tester une interface de manière automatisée....
Définir l’interface à tester avec createComposeRule
Tester en dehors d’une activité
Le plus gros avantage en utilisant Jetpack Compose est qu’il n’est pas nécessaire de démarrer une activité de l’application pour pouvoir tester son interface.
Pour cela, il faut :
-
Instancier un objet de type ComposeContentTestRule grâce à la fonction createComposeRule().
-
Déclarer l’interface à tester avec la fonction setContent fournie par l’interface ComposeContentTestRule. Cette fonction prend en paramètre une fonction Composable. C’est dans cette fonction que le contenu à tester doit être déclaré.
En appliquant scrupuleusement le modèle de conception Unidirectional Data Flow (UDF), chaque composant de l’interface d’une application est alors testable unitairement.
Pour rappel le modèle UDF a été introduit dans le chapitre Gestion des états et des effets. Il implique que seul le composant racine d’un écran possède une référence au ViewModel. Ainsi, pour tous les composants enfants d’un écran, les états et les événements dont ils ont besoin sont accessibles via leurs paramètres.
Pour définir la fonction Composable à tester, il suffit, dans la fonction setContent, de déclarer un composant en utilisant, si besoin, des données de test dans ses paramètres. Ainsi, sauf potentiellement pour le composant racine, il n’est pas nécessaire par exemple d’instancier un faux ViewModel ou de le mocker avec une bibliothèque tierce.
Voici comment initialiser une classe de test, dont le but est de valider le comportement de l’écran d’une conversation dans notre application de messagerie :
class ConversationTest...
Les fonctions permettant de tester une interface
Rechercher, actionner, vérifier
Maintenant que l’interface à tester est correctement définie, il faut pouvoir interagir avec et vérifier son contenu. Pour cela, Jetpack Compose fournit plusieurs fonctions pouvant être catégorisées comme ceci :
-
Des fonctions de recherche : elles permettent de rechercher un ou plusieurs éléments dans l’interface selon un critère, par exemple les fonctions onNode WithContentDescription, onNodeWithText, onAllNodesWith Text, etc.
-
Des fonctions d’action : elles permettent d’actionner les éléments de l’interface, par exemple les fonctions performClick, performScrollTo, performTextInput, etc.
-
Des fonctions d’assertion : elles permettent de vérifier si une condition donnée est vraie ou pas, par exemple les fonctions assertExists, assertContentDescriptionContains, assertIsToggleable, etc. Si l’assertion échoue, alors le test ne passe pas.
Illustrons l’utilisation de certaines de ces fonctions en testant l’écran de conversation de notre application de messagerie. Lorsque l’utilisateur clique sur le bouton permettant d’ajouter un émoji, vérifions que le sélecteur d’émoji est bien visible à l’écran. Cela donne l’extrait de code suivant :
class ConversationTest {
@get:Rule
val composeTestRule = createComposeRule()
@Before
fun setupConversationTest() {
/*...*/
}
@Test
fun userClickOnEmojiButton_emojiSelectorIsDisplayed() { ...
Tester au sein d’une activité
Il arrive d’avoir besoin de tester une fonctionnalité au sein d’une activité, et ce, généralement pour deux raisons :
-
pour accéder aux ressources de l’application
-
pour réaliser un test de bout en bout d’une activité
Dans ce genre de situation, il faut utiliser la fonction createAndroidComposeRule() à la place de la fonction createComposeRule() étudiée précédemment. Cette fonction prend en paramètre l’activité à tester. Par exemple, pour tester une activité de notre projet qui s’appelle MainActivity, il faut écrire createAndroidComposeRule<MainActivity>().
Accéder aux ressources
Illustrons l’usage de la fonction createAndroidComposeRule dans le cas où seul l’accès aux ressources de l’application est nécessaire. Dans ce genre de situation, il suffit d’utiliser une activité vide, par exemple ComponentActivity. Cela évite de devoir instancier une activité de l’application et de se soucier potentiellement de devoir mocker les données ainsi que la logique métier auxquelles elle accède.
Dans le test userClickOnEmojiButton_emojiSelectorIsDisplayed défini dans la section Rechercher, actionner, vérifier de ce chapitre, la recherche d’éléments de l’interface est effectuée en utilisant des chaînes de caractères non localisées, comme ceci :
@Test
fun userClickOnEmojiButton_emojiSelectorIsDisplayed() {
// WHEN
composeTestRule
.onNodeWithContentDescription("Ajouter un emoji")
.performClick()
//...
Arbre sémantique
L’API de test de Jetpack Compose utilise l’arbre sémantique pour interagir avec les éléments d’une interface. Pour rappel, l’arbre sémantique est en quelque sorte l’ombre de l’arbre de composition. Il possède la même structure que celui-ci mais chacun de ses nœuds contient uniquement les informations utiles pour décrire l’interface par les services d’accessibilité.
Deux versions de l’arbre sémantique : fusionné et non fusionné
Il existe en réalité deux versions de l’arbre sémantique :
-
L’arbre sémantique fusionné ou merged semantics tree en anglais.
-
L’arbre sémantique non fusionné ou unmerged semantics tree en anglais.
En effet, dans certains cas, il est pertinent de fusionner une partie des nœuds de l’arbre afin de les traiter de manière unifiée. Prenons le cas du composant Button. Il peut contenir plusieurs éléments comme une icône et un texte. Pourtant, au sein de l’arbre fusionné, il sera représenté par un nœud unique.
L’arbre sémantique non fusionné est utilisé par les services d’accessibilité. Ils vont en effet appliquer leur propre logique de fusion en se basant sur la propriété mergeDescendant de chaque composant.
L’arbre sémantique fusionné est utilisé par défaut par l’API de test de Compose. Cependant, il est possible dans un test d’utiliser l’arbre non fusionné.
Imprimer l’arbre sémantique dans les logs pour déboguer
Pour imprimer l’arbre sémantique au sein d’un test dans la console Logcat de Android Studio, il faut utiliser la méthode printToLog(). Cette fonction est particulièrement...
Conclusion
Avec le UI Toolkit original, tester unitairement une interface n’est pas toujours simple, notamment dû à la complexité de développer des composants dits "custom". La simplicité de développer un composant réutilisable avec Jetpack Compose facilite grandement l’écriture de test d’interface, en particulier en appliquant correctement le modèle de conception UDF. Pour conclure, il est intéressant de constater l’avantage de l’utilisation de l’arbre sémantique par les services d’accessibilité et par la bibliothèque de tests de Jetpack Compose. En effet, grâce à cela, lorsque nous améliorons l’accessibilité d’une application, alors la testabilité de celle-ci est améliorée, et réciproquement.