React Testing Library

React & Composants Partie III

Olivier YOUF
Just-Tech-IT
12 min readOct 15, 2020

--

Photo du cap blanc nez avec quelques petits moutons en premier plan
Moutons au Cap Blanc Nez — Photo Olivier YOUF

Voici le dernier article de la série sur les composants React. Pour rappel, nous avons vu ensemble :

Vous avez sans doute suivi également l’important article parlant de l’accessibilité. Vous comprendrez dans quelques paragraphes pourquoi cet article a été écrit avant cette toute dernière partie.

N’écrivez pas trop de tests, et principalement des tests d’intégrations.

Le trophée des tests

Trophée des tests

J’en ai parlé un peu dans mon article sur comment tester son application et Kent C. Dodds en parle plus en profondeur dans un super article : au delà des tests unitaires, il est indispensable de couvrir efficacement son application avec des tests d’intégration . Et ce pour plusieurs raisons :

  • 🤜🏻 On va tester efficacement qu’un composant fait exactement ce qu’attend l’utilisateur. Plus que couvrir du code, on va couvrir les cas d’utilisations. On va lister les scénarios et les tester.
  • 💨 Le test va être résiliant à la refactorisation qui est par définition une modification du code sans impact sur les fonctionnalités.
  • 🤝 Augmenter sensiblement la confiance dans ce qui est produit. Quand on sait que le composant est sécurisé par ses tests, on peut se reposer dessus.
  • 💰 Plus couteux (un peu) qu’un test unitaire mais moins que le test End-To-End

On peut résumer les tests d’intégration en une seule image.

Test Unitaire 1 : les porte s’ouvrent ✅

C’est pour tester l’intégration de nos composants que nous allons utiliser React Testing Library

Contexte

Cet article a été élaboré en partant sur un projet créé avec Create-React-App. Vous trouverez toutes les sources de mes exemples, et mes travaux concernant cet ensemble d’articles sur mon Github

Partant de ce projet simple, nous allons tester les différents éléments de nos composants, notamment la vue, le container, les hooks, puis enfin un provider. Si cela ne vous est pas familier, vous pouvez jeter un oeil au premier article sur la composition de composants.

Nous n’en parlons pas ici, mais cette méthode peut être utilisée, et l’est quand cela fait sens au sein de nos projets, avec la méthodologie TDD.

Npm Install

React Testing library n’est pas seulement une librairie, c’est un vrai écosystème. Nous allons voir à travers nos couches, les différentes façons de couvrir notre application.

C’est la base, ce qui va nous permettre d’utiliser le render de composant.

Cette partie va nous permettre de simuler les actions de l’utilisateur.

Ici c’est plus une boite à outil, vraiment très utile. Elle va venir enrichir les fonctions disponibles pour valider nos tests.

Le package react hooks sera utile uniquement pour tester des hooks réutilisables. Pour les hooks des containers, nous les testerons directement au sein de leur composant avec React Testing Library.

Ce package nécessite react-test-renderer qu’on doit installer en plus.

Une fois ces librairies installées, en fonction de vos besoins, nous pouvons faire une toute petite configuration dans le fichier setupTests.js

Cet import, nécessaire pour utiliser les extensions de Jest-dom, sera de ce fait effectué pour chaque test.

❗️Dernier détail, si vous utilisez Create React App, ce dernier embarque la version de Jest 24. Cependant la librairie de tests nécessite Jest 26 (pour l’utilisation de waitFor par exemple). De ce fait vous devez embarquer jest-environment-jsdom-sixteen

Puis l’ajouter dans les scripts de lancement de tests dans votre package.json :

Cette dernière partie ne devrait plus être nécessaire avec la V4 de react-script qui intégrera Jest26.

La vue

La vue va se tester de plusieurs façons. Dans un premier temps nous allons poser un filet de sécurité pour alerter le développeur en cas de modification : Le snapshot.

Le snapshot est un sujet qui déchaine les foules. Il est pourtant assez mineur et ne représente qu’une petite partie des tests. Il a juste un rôle d’avertissement. Sur une vue simple il sera suffisant, sur une vue plus spécifique, nous le verrons ensuite, il devra être accompagné.

Ici nous posons notre filet, nous voulons que ce composant soit figé.

Ce qui est bien avec le JSX c’est que nous pouvons poser, au besoin des conditions, des mappers, des boucles etc.

Afin de vérifier et tester correctement ces éléments nous devons donc accompagner le snapshot. La lecture du test doit indiquer ce que nous vérifions, et le snapshot ne le permet pas.

Ici nous avons une condition et une liste, vérifions tout ça.

A la simple lecture des tests, nous comprenons de suite ce que nous vérifions.

Dans un premier temps nous montons le composant. Le screen représente le body contenant le composant, il sera notre base de travail. Le getByRole/getByText est notre requête. Vient ensuite l’assertion.

Les requêtes

Cette requête est composée de deux choses : le comment et le quoi.

Le comment est la première partie. Nous allons indiquer comment récupérer ce que nous voulons chercher. Nous avons :

  • get[All]By : Ici nous voulons récupérer un élément. Si la requête échoue si aucun ou plusieurs éléments sont trouvés. Si nous ajoutons All nous indiquons que nous voulons tous les éléments trouvés qui seront renvoyés sous forme de liste.
  • query[All]By : Fonctionne de la même façon que le get, sauf que s’il ne trouve pas d’élément, il renverra null. Utile pour tester la non existence d’un élément.
  • find[All]By : Fonctionne de la même façon que le get, à la différence qu’il va renvoyer une promesse qui sera résolue une fois l’élément trouvé ou le timeout passé. Outil tout désigné pour tester un appel de service par exemple.

Le quoi va permettre de cibler l’élément. Pour cela il y a une multitude de requêtes. Nous allons en voir les principales, par leur ordre d’importance.

  • ByRole : C’est LA requête a privilégier. Ici nous allons accéder à l’élément par son rôle (bouton, champ, liste…) souvent accompagné de son name. Cette requête se base sur l’accessibilité de l’élément. Pour plus de détails vous pouvez vous reporter à mon précédent article sur l’accessibilité.
  • ByLabelText : C’est la méthode à privilégier pour récupérer des champs de formulaire.
  • ByText : Nous allons ici pouvoir récupérer un élément en général non interactif, par rapport à son contenu, par exemple une div ou un span.

A ne briser qu’en cas d’urgence

Photo by Dave Phillips on Unsplash

Les deux requêtes suivantes sont à éviter autant que faire se peut. Dans certains cas, vous n’aurez pas le choix. Mais le plus souvent les méthode cités précédemment seront utilisables.

  • Requête manuelle : Lors de l’appel à la méthode render, vous pouvez récupérer dans le résultat le container. Vous pouvez pour cela utiliser l’API query selector.
  • ByTestId : Lors de l’écriture de l’HTML du composant, vous pouvez ajouter l’attribut data-testid. Vous pourrez ainsi y accéder facilement dans vos tests.

Pour plus de détails sur les requêtes et comment les utiliser, voici un super guide pour choisir votre requête.

Les assertions strictes avec JestDOM

Une fois que l’on a notre élément en main, nous pouvons tester et effectuer notre assertion stricte. Par exemple pour vérifier qu’un bouton est désactivé :

Avec JestDOM, nous allons écrire plutôt :

En plus de favoriser la lecture, il facilite l’écriture, et le déboggage des tests à l’aide de messages plus explicites.

Rien ne sert d’en faire une liste exhaustive ici, la documentation est simple, et la lecture des noms des fonctions suffira bien souvent à savoir ce que cela teste.

Pour rappel, l’import de la librairie peut se faire au début de chaque test ou globalement dans le fichier setupTests.js.

Les containers

C’est ici que se passe la partie la plus interessante a mon goût. Je vais vous donner ma méthode globale, ce n’est pas forcément la meilleure, mais pour le moment elle fonctionne bien.

Une fois la vue montée, elle se passe en 3 étapes :

  • Je crée mon test avec la liste des fonctionnalités que je cherche à couvrir. Comme exemple, prenons un formulaire. Il doit valider les champs à la sortie, prendre en compte les modifications saisies, valider un formulaire à la soumission puis enfin l’envoyer quand tout se passe bien (ou afficher les erreurs le cas échéant).
    🔬 Je cherche avant tout à couvrir tous les cas, sans me concentrer sur le code.
  • Les tests écris, je passe à l’écriture du code. Ces deux premières étapes peuvent être inversées, en fonction que vous fassiez ou non du TDD.
    🚦En TDD le but va être de faire passer les tests au vert.
  • Quand tout est au vert, et que mon composant est terminé, je lance la couverture de code. Cette dernière étape va me permettre de detecter deux choses : les cas non couverts et le code inutile. Chaque ligne remontée sera dans un cas ou dans l’autre.
    🛠 Le travail est terminé, les tests sont posés, on peut refactoriser le code sans toucher aux tests.

Pour monter un container, tout se passe comme pour les vues. Nous montons tout d’abord le composant avec render.

Puis nous allons, contrairement avec les vues, cette fois-ci interagir avec lui. Et pour cela nous allons utiliser le screen pour récupérer les éléments du DOM et @testing-library/user-event pour simuler les actions de l’utilisateur. Les plus communs sont le click et le type, mais vous avez d’autres actions possibles que je vous invite à consulter au besoin.

Il ne reste plus ensuite que de faire les assertions habituelles.

Ben y a rien !

On attend…

Effectivement si on pose simplement l’assertion :

L’assertion va échouer. Regardons pourquoi avec un

Qui va nous afficher le contenus du DOM.

Cette fonction permet également de débugger simplement un élément, et vous sera très très utile pour débugger vos tests.

On voit qu’à la place du résultat on a

Ok donc la réponse est facile, le fetch n’est pas terminé.

Plutôt que de vouloir attendre le fetch, pensons utilisateur. On veut attendre que le loading ne soit plus affiché. Pour cela nous allons utiliser WaitFor.

Cette fonction est LA fonction à tout faire. On peut attendre l’affichage d’un élément, une assertion, un appel, etc.

Avant cette fonction il était assez compliqué de jouer avec tout ce qui était asynchrone. Maintenant on doit juste lui dire ce qu’on attend :

Littéralement on peut lire

J’attend que l’assertion “Le loader n’est plus affiché” soit vraie.

Dans ce cas précis (on attend qu’un élément disparaisse) on peut également utiliser une autre fonction : waitForElementToBeRemoved.

Pour rappel, pour rechercher un élément, nous pouvons utiliser également la requête findBy, qui est une concaténation simple d’un waitFor et d’un getBy

Dans le meilleur des mondes

Jusqu’à maintenant, nous avons fais des tests sur des cas simples et totalement indépendants. Mais souvent les composants riches s’inscrivent dans un contexte applicatif. Nous allons vouloir passer à la page suivante lors d’une validation de formulaire, ou encore accéder ou enregistrer un résultat dans un store redux ou dans un contexte. Dans ce cas, notre container va contenir un useDispatch, un useContext ou encore un useHistory.

Pour les tests, oublions de suite la solution consistant à mocker ces hooks (on ne teste pas l’implémentation !!). Il est très compliqué de mocker un hook. Plutôt que de mocker le hook, nous allons mocker les points d’entrée/sortie. La solution va donc être de wrapper le composant, et de simuler les providers.

Pour wrapper son composant, nous allons utiliser le second paramètre de la fonction render. Celle ci permet de spécifier un wrapper. Prenons l’exemple simple du contexte. Dans notre application exemple, nous avons un provider gérant les messages d’erreurs.

Le wrapper contient ici le provider qui sera celui va contenir nos mocks. Nous pourrons ainsi vraiment tester l’intégration de notre composant.

Voici de la même manière une façon de mocker le router

Et une façon de mocker Redux

Tester les hooks

Quand nous écrivons un container, nous sommes très rapidement amener à écrire des custom hooks. Dans certains cas, nous allons vouloir tester les hooks, comme nous testerions des fonctions pures. Sauf qu’ici ça n’en est pas. Et donc Jest n’est plus suffisant.

☝🏻 Cependant nous ne devons tester les hooks que dans certains cas :

  • Quand il est compliqué de le tester directement à travers le container, ou quand il représente une règle métier.
  • Quand il n’est pas relié à un composant en particulier, pour un hook générique par exemple.

Tester un hook utilisé uniquement pour un unique composant revient à tester l’implémentation. Ce que nous voulons éviter.

Et pour ce faire nous allons utiliser la librairie react-hooks-testing-library

Dans un premier temps nous montons le Hook, de la même manière que pour un composant.

Ici result va contenir un objet mutable current qui est l’état courant de ce qui est retourné par le hook. Il contiendra aussi error qui lui s’occupera de renvoyer les exceptions levées.

Avec cet objet nous pouvons déjà faire plusieurs tests, comme par exemple dans notre application, le useInput qui sert a représenter l’ensemble des champs du formulaire et de ses fonctions d’accès :

Logiquement le act est nécessaire dès que nous jouons avec un composant. Il sert a préparer le composant dans un environnement proche de celui du navigateur. Cependant il est très souvent déjà inclus (comme les userEvent.* ou les waitFor etc.). Cependant ici ce n’est pas le cas, nous devons donc l’ajouter explicitement.

Le render hooks fournis également plusieurs autre fonctions utiles

  • Unmount : permet de simuler la suppression du composant dans le dom. Utile notamment pour le useEffect qui contient une callback pour ce cas.
  • Rerender : Permet de simuler la transmission de nouvelle propriétés dans le hook, afin de tester sa mutation.

WaitForNextUpdate, waitForValueToChange, WaitFor : Ici ce sont des fonctions qui vont permettre de jouer avec les appels asynchrones du hook. Comme très souvent nous en trouverons l’utilité avec un fetch.

🕵🏻‍♂️ Attention cependant, le hook doit retourner un état sur lequel la librairie peut se baser (par exemple ci dessus users) pour détecter la mise à jour asynchrone.

Le cas échant, on peut attendre une mise à jour à l’aide de l’option interval

Tester les provider

Comme nous avons pu le constater, nous avons régulièrement dans nos applications, des providers qui vont distribuer des méthodes et des états à nos différentes couches en utilisant l’API context. Nous savons comment simuler ces providers, mais maintenant intéressons nous à la façon dont nous pouvons les couvrir par les tests.

Je me suis longtemps demandé comment tester l’intégration d’un provider, et en réalité la réponse est assez simple : il suffit de l’utiliser et d’en vérifier le résultat. Pour cela, notre test va simplement contenir un composant factice très simple qui va importer l’ensemble des états et des fonctions proposés par notre provider.

That’s It

L’article est certes relativement long. Cependant, je pense avoir listé les cas les plus courants, ainsi que les soucis que j’ai le plus souvent rencontrés.

N’hésitez pas à garder l’article sous la main, ainsi que le repo Github associé. Souvent quand je rencontre un problème j’essaie de le reproduire dans ce petit projet et donc de mettre la résolution avec.

Photo by Chris Hardy on Unsplash

🎉 Comme d’habitude, si vous avez apprécié cet article, commentez, clapez, partagez ! Ca me motivera beaucoup à en faire d’autres. 🎉

--

--

Olivier YOUF
Just-Tech-IT

I am a FrontEnd developer. My speciality is React JS but I am sensitive to all. I love running, especially in mountains.