Testez l’UI de votre app React Native avec Detox
Ça y est, vous venez de terminer votre user story mobile et vous l’annoncez fièrement à votre équipe en daily meeting le lendemain matin. Oui mais voilà, votre collègue de l’équipe backend vous lance innocemment : “Super ça, mais tu as pensé à écrire des tests ?”. Tout penaud, vous sortez timidement : “Euh, tu sais, les tests, sur une app mobile, c’est pas simple. Une app mobile, c’est beaucoup d’UI, et l’UI c’est compliqué à tester”. Et vous n’avez pas complètement tort.
La solution la plus utilisée pour tester une app React Native est Jest, associée parfois à Enzyme. Jest est un framework qui permet d’écrire des tests à l’aide d’assertions et de mocks, et Enzyme permet de manipuler et d’inspecter simplement l’arbre de vues d’un composant React. Ce type de tests fonctionne très bien pour les briques logiques d’une application ou pour les composants simples, mais dès lors que l’on souhaite tester les composants avancées comme les écrans, l’effort à fournir parait très vite décourageant : il est souvent nécessaire d’écrire de nombreux mocks pour l’accès aux fonctionnalités du téléphone (stockage local, cycle de vie de l’app, notifications, etc…), pour les appels réseaux avec le backend, ou encore pour les libs natives utilisées qui n’en fournissent que rarement. Ces mocks ajoutent un coût de maintenance non négligeable et limitent par ailleurs grandement la pertinence des tests réalisés. Enfin, ces tests s’exécutent sur la machine du développeur ou de la CI, sans rendu graphique, dans un environnement d’exécution très éloigné de celui d’un iPhone ou d’un téléphone Android, ce qui limite encore plus leur intérêt.
Une solution à ces problèmes consiste à tester l’application dans sa globalité, en la faisant tourner sur un appareil physique ou dans un simulateur. Les tests interagissent alors directement avec les composants graphiques à la manière d’un utilisateur final et vérifient que le résultat à l’écran correspond bien à ce qui est attendu. Ce type de tests, appelés tests end-to-end ou encore tests UI, permet de couvrir rapidement un grand nombre de fonctionnalités, y compris les échanges avec le backend.
Présentation de Detox
Dans la suite de cet article, nous allons partir à la découverte de Detox, une solution de tests end-to-end conçue pour les applications React Native. Detox possède entre autres les fonctionnalités suivantes :
- Récupérer un composant graphique de l’app à partir de différents critères : son id, le texte qu’il affiche, ou encore son parent ou ses enfants ;
- Réaliser des actions sur un composant graphique comme un appui, une saisie de texte, une action de défilement ou des gestures (swipe et pinch) ;
- Réaliser des assertions sur la visibilité ou encore le texte affiché par un composant ;
- Simuler le passage en background/foreground du téléphone, la réception d’une notification, l’ouverture de l’app via une URL, etc…
Setup de Detox
Le setup de Detox dans une application React Native se fait assez simplement. Commençons tout d’abord par installer son CLI sur la machine de test :
# applesimutils est utilisé par le CLI de Detox
# pour lancer et contrôler le simulateur iOS
brew tap wix/brew
brew install applesimutils
npm install -g detox-cli
Ajoutons ensuite Detox à notre projet React Native :
npm install -D detox
Et ajoutons la configuration de l’environnement d’exécution des tests Detox en ajoutant le bloc suivant au fichier package.json
de l’app :
Note : Dans le cas d’un projet utilisant Cocoa Pods, remplacez -project ios/MonApp.xcodeproj
par -workspace ios/MonApp.xcworkspace
Ici, nous configurons un simulateur d’iPhone 7 comme environnement d’exécution. Il est également possible d’exécuter les tests sur émulateur Android ou sur le device Android branché à la machine. L’exécution sur device iOS n’est cependant pas encore supportée.
Pour finir, utilisons le CLI de Detox afin de configurer Jest comme runner des tests :
detox init -r jest
Cette étape ajoute également un répertoire e2e
contenant entre autres le fichier firstTest.spec.js
qui décrit les tests à exécuter. Avant de jeter un œil à ce fichier, compilons et lançons les tests sur notre application :
detox build
detox test
Si tout s’est bien déroulé, le simulateur iOS devrait démarrer, votre app devrait s’installer et se lancer, et les 2 tests du fichiers firstTest.spec.js
devraient s’exécuter (et échouer logiquement).
Anatomie d’un test Detox
Pour comprendre comment écrire des tests end-to-end avec Detox, ouvrons le fichier firstTest.spec.js
:
Même sans avoir lu la documentation de Detox, les tests sont très simples à comprendre. Chaque test est déclaré grâce à la fonction it()
de Jest et se compose d’une série d’instructions où l’on identifie un élément graphique element(by.id("actionButton"))
et l’on réalise soit une action monElement.tap()
, soit une assertion sur son état expect(monElement).toBeVisible()
.
Identification des éléments
Comme vu dans l’exemple précédent, une façon d’identifier un élément graphique est d’utiliser la fonction by.id("actionButton")
. L’id passé à cette fonction correspond à la valeur de la props testID
de l’élément (e.g. <Button testID="actionButton" />
). Cette méthode présente donc l’avantage de cibler de manière fiable un élément graphique, mais nécessite cependant de modifier le code de l’application pour ajouter les props testID
sur les éléments qui l’on souhaite manipuler.
D’autres fonctions permettent d’identifier un élément en se basant sur d’autres critères comme le texte affiché par l’élémentby.text("Se connecter")
, la classe native du composant by.type("RCTScrollView")
ou encore son parent by.id("actionButton").withAncestor(by.id("loginScreen"))
.
Enfin, il est possible de combiner plusieurs critères à l’aide de la fonction and()
ou de récupérer le n-ième élément répondant à un critère element(by.id("cellView")).atIndex(3)
.
Actions sur un élément
Une fois l’élément identifié, des actions peuvent être réalisées pour intéragir avec lui. Les 2 actions les plus fréquentes sont l’appui tap()
sur un élément cliquable ou la saisie de texte typeText("user@gmail.com")
sur un champ texte.
Il est également possible de réaliser des gestures comme un appui long longPress()
, un défilement sur une scroll-view scroll(100, "down")
ou encore un pinch pinchWithAngle("outwards", "slow", 90)
.
Assertion sur un élément
Lorsque l’on écrit un test end-to-end, il est souvent utile de vérifier qu’à la suite d’une série d’actions, un élément graphique est présent à l’écran.
Detox permet de réaliser ce type d’assertions à l’aide de fonctions commeexpect(element(by.id("successMessage"))).toBeVisible()
qui permet de vérifier qu’un élément est visible à l’écran (i.e. non recouvert par une vue opaque et dans la zone affichée par l’écran).
D’autres fonctions permettent de tester la présence dans l’arbre de rendu toExist()
ou encore de vérifier le texte affiché par l’élément toHaveText("Succès")
.
Et la synchronisation dans tout ça ?
Si vous avez déjà écrit des tests end-to-end avec d’autres solutions (Appium par exemple), une question que vous devez probablement vous poser est comment fonctionne la synchronisation des différentes étapes d’un test Detox. En effet, on ne peut pas se contenter d’exécuter les étapes séquentiellement les unes après les autres, sans délai, car l’apparition de certains éléments dépend souvent de la réponse d’un appel réseau, d’un timer ou encore d’une animation. Une solution à ce problème pourrait être d’imposer un délai par défaut entre deux étapes consécutives, mais cette solution introduit de l’instabilité dans l’exécution des tests (tests qui échouent car un appel réseau a pris plus de temps qu’habituellement par exemple).
La solution proposée par Detox est de monitorer les appels réseau, les animations et les timers et de n’exécuter l’étape suivante que lorsqu’il n’y a plus rien en cours. Cette solution permet d’éviter les cas de faux négatifs qui se produisent parfois avec d’autres solutions.
Enfin, il est toujours possible de forcer la synchronisation en se basant sur un critère particulier. Par exemple, si l’on veut forcer l’attente tant qu’un élément n’est pas visible à l’écran, il est possible d’écrire :
waitFor(element(by.id(“actionButton”)).toBeVisible().withTimeout(2000);
Ce genre de construction n’est cependant que rarement utile, la synchronisation automatique de Detox étant plutôt efficace.
Detox, une solution parfaite ?
Sur le papier, Detox semble être l’outil parfait pour réaliser des tests end-to-end sur votre app : l’écriture des tests est très simple et ne nécessite que très peu de modifications du code existant. Par ailleurs, l’exécution des tests semblent plus fiables qu’avec d’autres solutions grâce aux mécanismes de synchronisation de Detox. Mais en pratique, qu’en est-il ?
Malheureusement, tout n’est pas si rose :
- Il existe tout de même quelques rares cas où une synchronisation manuelle reste nécessaire ;
- Il n’est pas rare qu’une app React Native soit dans un état instable après un grand nombre de reload. Hors, par défaut, Detox reload votre app avant l’exécution de chaque test et la probabilité que l’app devienne instable augmente donc avec le nombre de tests écrits.
Heureusement, il est très souvent possible de trouver des workarounds à ces problèmes mais cela rend l’écriture des tests plus fastidieuses.
Malgré cette conclusion en demi-teinte, j’espère vous avoir donné envie d’essayer Detox. N’hésitez pas à nous faire part de votre expérience ou de vos remarques dans les commentaires 👇