Comment tester vos applications React avec la bibliothèque de tests React
À propos de l'auteur
Développeur frontend génial qui aime tout le codage. Je suis un amoureux de la musique chorale et je travaille pour la rendre plus accessible au monde, un téléchargement à la… En savoir plus sur Chidi …
Les tests donnent confiance au code écrit. Dans le contexte de cet article, «test» signifie «test automatisé». Sans test automatisé, il est beaucoup plus difficile d'assurer la qualité d'une application Web d'une complexité importante. Les échecs causés par les tests automatisés peuvent entraîner davantage de bogues en production. Dans cet article, nous allons montrer comment les développeurs React peuvent rapidement commencer à tester leur application avec la Bibliothèque de tests React (RTL).
Aujourd'hui, nous allons brièvement expliquer pourquoi il est important d'écrire de manière automatisée teste n'importe quel projet logiciel et met en lumière certains des types courants de tests automatisés. Nous allons créer une application de liste de tâches en suivant l'approche de développement piloté par les tests (TDD). Je vais vous montrer comment écrire à la fois des tests unitaires et fonctionnels, et dans le processus, expliquer ce que sont les simulations de code en se moquant de quelques bibliothèques. J'utiliserai une combinaison de RTL et Jest – qui sont tous deux préinstallés dans tout nouveau projet créé avec Create-React-App (CRA).
Pour suivre, vous devez savoir comment configurer et naviguer dans un nouveau projet React et comment travailler avec le gestionnaire de paquets de fils (ou npm). Des connaissances avec Axios et React-Router sont également requises.
Meilleures pratiques avec React
React est une bibliothèque JavaScript fantastique pour créer des interfaces utilisateur riches. Il fournit une excellente abstraction de composants pour organiser vos interfaces en un code qui fonctionne bien, et il y a à peu près tout ce que vous pouvez l'utiliser. Lire plus d'articles sur React →
Pourquoi vous devriez tester votre code
Avant d'envoyer votre logiciel aux utilisateurs finaux, vous devez d'abord confirmer qu'il fonctionne comme prévu. En d'autres termes, l'application doit satisfaire aux spécifications de son projet.
Tout comme il est important de tester notre projet dans son ensemble avant de l'envoyer aux utilisateurs finaux, il est également essentiel de continuer à tester notre code pendant la durée de vie d'un projet. Cela est nécessaire pour plusieurs raisons. Nous pouvons apporter des mises à jour à notre application ou refactoriser certaines parties de notre code. Une bibliothèque tierce peut subir un changement de rupture. Même le navigateur qui exécute notre application Web peut subir des changements de rupture. Dans certains cas, quelque chose cesse de fonctionner sans raison apparente – les choses pourraient mal se produire de façon inattendue. Ainsi, il est nécessaire de tester régulièrement notre code pendant toute la durée de vie d'un projet.
D'une manière générale, il existe des tests logiciels manuels et automatisés. Dans un test manuel, un utilisateur réel effectue une action sur notre application pour vérifier qu'elle fonctionne correctement. Ce type de test est moins fiable lorsqu'il est répété plusieurs fois car il est facile pour le testeur de manquer certains détails entre les tests.
Dans un test automatisé, cependant, un script de test est exécuté par une machine. Avec un script de test, nous pouvons être sûrs que les détails que nous définissons dans le script resteront inchangés à chaque exécution de test.
Ce type de test nous offre les avantages d'être prévisible et rapide, de sorte que nous pouvons trouver et corriger rapidement
Ayant vu la nécessité de tester notre code, la prochaine question logique est, quel genre de tests automatisés devrions-nous écrire pour notre code? Passons rapidement en revue quelques-uns d'entre eux.
Types de tests automatisés
Il existe différents types de tests logiciels automatisés. Certains des tests les plus courants sont les tests unitaires, les tests d'intégration, les tests fonctionnels, les tests de bout en bout, les tests d'acceptation, les tests de performances et les tests de fumée.
Test unitaire Dans ce type de test, l'objectif est pour vérifier que chaque unité de notre application, considérée isolément, fonctionne correctement. Un exemple serait de tester qu'une fonction particulière retourne une valeur attendue, donner quelques entrées connues. Nous allons voir plusieurs exemples dans cet article.
Test de fumée Ce type de test est effectué pour vérifier que le système est opérationnel. Par exemple, dans une application React, nous pourrions simplement afficher notre composant d'application principal et l'appeler un jour. S'il s'affiche correctement, nous pouvons être assez certains que notre application s'affichera sur le navigateur.
Test d'intégration Ce type de test est effectué pour vérifier que deux modules ou plus peuvent bien fonctionner ensemble. Par exemple, vous pouvez exécuter un test pour vérifier que votre serveur et votre base de données communiquent réellement correctement.
Test fonctionnel Un test fonctionnel existe pour vérifier que le système répond à ses spécifications fonctionnelles. Nous verrons un exemple plus tard.
Test de bout en bout Ce type de test implique de tester l'application de la même manière qu'elle serait utilisée dans le monde réel. Vous pouvez utiliser un outil comme cyprès pour les tests E2E.
Test d'acceptation Ceci est généralement effectué par le propriétaire de l'entreprise pour vérifier que le système répond aux spécifications.
Test de performance Ce une sorte de test est effectuée pour voir comment le système fonctionne sous une charge importante. Dans le développement frontend, il s'agit généralement de la vitesse à laquelle l'application se charge sur le navigateur.
Quand cela arrive pour tester les applications React, il existe quelques options de test disponibles, dont les plus courantes que je connais sont Enzyme et React Testing Library (RTL).
RTL est un sous-ensemble de la famille de packages @ testing-library . Sa philosophie est très simple. Vos utilisateurs se moquent de savoir si vous utilisez redux ou le contexte pour la gestion des états. Ils se soucient moins de la simplicité des crochets ni de la distinction entre classe et composants fonctionnels. Ils veulent juste que votre application fonctionne d'une certaine manière. Il n'est donc pas surprenant que le principal principe directeur de la bibliothèque de tests soit
«Plus vos tests ressemblent à la façon dont votre logiciel est utilisé, plus ils peuvent vous donner confiance.»
, quoi que vous fassiez, pensez à l'utilisateur final et testez votre application comme il l'utiliserait.
Le choix de RTL vous offre un certain nombre d'avantages. Tout d'abord, il est beaucoup plus facile de commencer avec. Chaque nouveau projet React démarré avec CRA est livré avec RTL et Jest configuré. Les documents React le recommandent également comme bibliothèque de test de choix. Enfin, le principe directeur a beaucoup de sens – la fonctionnalité sur les détails d'implémentation.
Cela étant terminé, commençons par créer une application de liste de tâches, en suivant l'approche TDD.
Configuration du projet [19659002] Ouvrez un terminal et copiez et exécutez la commande ci-dessous.
# start new react project and start the server
npx create-react-app start-rtl && cd start-rtl && yarn start
Cela devrait créer un nouveau projet React et démarrer le serveur sur http: // localhost: 3000 . Une fois le projet en cours, ouvrez un terminal séparé, exécutez test de fil puis appuyez sur a . Ceci exécute tous les tests du projet en mode montre . L'exécution du test en mode veille signifie que le test sera automatiquement réexécuté lorsqu'il détectera une modification du fichier de test ou du fichier en cours de test. Sur le terminal de test, vous devriez voir quelque chose comme l'image ci-dessous:
Vous devriez voir beaucoup de verts, ce qui indique que le test que nous effectuons a passé avec brio.
Comme je l'ai mentionné plus tôt, l'ARC configure RTL et Jest pour chaque nouveau projet React. Il comprend également un exemple de test. Cet exemple de test est ce que nous venons d'exécuter.
Lorsque vous exécutez la commande yarn test react-scripts appelle Jest pour exécuter le test. Jest est un framework de test JavaScript utilisé pour exécuter des tests. Vous ne le trouverez pas dans package.json mais vous pouvez effectuer une recherche dans yarn.lock pour le trouver. Vous pouvez également le voir dans node_modules / .
Jest est incroyable dans la gamme de fonctionnalités qu'il fournit. Il fournit des outils pour les assertions, les moqueries, l'espionnage, etc. Je vous encourage fortement à faire au moins un petit tour de la documentation. Il y a beaucoup à apprendre là-bas que je ne peux pas gratter dans ce court morceau. Nous utiliserons beaucoup Jest dans les sections à venir.
Ouvrez package.json voyons ce que nous avons là-bas. La section qui nous intéresse est dépendances .
Ouvrez App.test.js prenons un aperçu de son contenu.
import React de 'react';
importer {render} depuis '@ testing-library / react';
importer l'application depuis './App';
test ('rend le lien apprendre réagit', () => {
const {getByText} = render ();
const linkElement = getByText (/ learn react / i);
attend (linkElement) .toBeInTheDocument ();
});
La méthode de rendu de RTL rend le composant et renvoie un objet qui est déstructuré pour la requête getByText . Cette requête trouve des éléments dans le DOM par leur texte d'affichage. Les requêtes sont les outils pour trouver des éléments dans le DOM. La liste complète des requêtes peut être trouvée ici . Toutes les requêtes de la bibliothèque de tests sont exportées par RTL, en plus des méthodes de rendu, de nettoyage et d'action. Vous pouvez en savoir plus à ce sujet dans la section API .
Le texte est associé à l'expression régulière / learn react / i . Le drapeau i rend l'expression régulière insensible à la casse. Nous nous attendons à ce que trouve le texte Learn React dans le document.
Tout cela imite le comportement d'un utilisateur dans le navigateur lorsqu'il interagit avec notre application.
commencer à apporter les modifications requises par notre application. Ouvrez App.js et remplacez le contenu par le code ci-dessous:
import React from "react";
import "./App.css";
fonction App () {
revenir (
Prise en main de la bibliothèque de test React
);
}
exporter l'application par défaut;
Si le test est toujours en cours d'exécution, vous devriez voir le test échouer. Vous pouvez peut-être deviner pourquoi c'est le cas, mais nous y reviendrons un peu plus tard. En ce moment, je veux refactoriser le bloc de test.
Remplacez le bloc de test dans src / App.test.js par le code ci-dessous:
# use describe, it pattern
décrire ("", () => {
il ("Rendu composant correctement", () => {
const {getByText} = render ();
expect (getByText (/ Prise en main de la bibliothèque de test React / i)). toBeInTheDocument ();
});
});
Ce refactor ne fait aucune différence matérielle sur la façon dont notre test se déroulera. Je préfère le modèle décrire et le modèle car il me permet de structurer mon fichier de test en blocs logiques de tests associés. Le test devrait recommencer et cette fois il passera. Au cas où vous ne l'auriez pas deviné, le correctif du test ayant échoué était de remplacer le texte learn react par Premiers pas avec la bibliothèque de tests React .
Au cas où vous ne le feriez pas ' Pour avoir le temps d'écrire vos propres styles, vous pouvez simplement copier celui ci-dessous dans App.css .
.App {
min-hauteur: 100vh;
alignement du texte: centre;
}
.App-header {
hauteur: 10vh;
affichage: flex;
couleur de fond: # 282c34;
flex-direction: colonne;
align-items: centre;
justifier-contenu: centre;
taille de police: calc (10px + 2vmin);
Couleur blanche;
}
.App-body {
largeur: 60%;
marge: 20px auto;
}
ul {
rembourrage: 0;
affichage: flex;
type de liste: décimal;
flex-direction: colonne;
}
li {
taille de police: grande;
alignement du texte: gauche;
rembourrage: 0,5rem 0;
}
li a {
transformation de texte: capitaliser;
décoration de texte: aucune;
}
.todo-title {
transformation de texte: capitaliser;
}
.terminé {
la couleur verte;
}
.pas achevé {
La couleur rouge;
}
Vous devriez déjà voir le titre de la page monter après avoir ajouté ce CSS.
Je considère que c'est un bon point pour moi de valider mes modifications et de pousser vers Github. La branche correspondante est 01-setup .
Continuons avec notre configuration de projet. Nous savons que nous allons avoir besoin de navigation dans notre application, nous avons donc besoin de React-Router. Nous effectuerons également des appels API avec Axios. Installons les deux.
# install react-router-dom et axios
yarn add react-router-dom axios
La plupart des applications React que vous allez créer devront conserver leur état. Il existe de nombreuses bibliothèques disponibles pour gérer l'état. Mais pour ce didacticiel, j'utiliserai l'API contextuelle de React et le crochet useContext . Configurons donc le contexte de notre application.
Créez un nouveau fichier src / AppContext.js et entrez le contenu ci-dessous.
Ici, nous créons un nouveau contexte avec React.createContext ({}) pour lequel la valeur initiale est un objet vide. Nous définissons ensuite un composant AppProvider qui accepte le composant enfants . Il encapsule ensuite ces enfants dans AppContext.Provider rendant ainsi l'objet {appData, appDispatch} accessible à tous les enfants, n'importe où dans l'arborescence de rendu.
Notre réducteur [Lafonction définit deux types d'actions.
LOAD_TODOLIST qui est utilisé pour mettre à jour le tableau todoList .
LOAD_SINGLE_TODO qui est utilisé pour mettre à jour activeToDoItem .
appData et appDispatch sont tous deux renvoyés du crochet useReducer . appData nous donne accès aux valeurs de l'état tandis que appDispatch nous donne une fonction que nous pouvons utiliser pour mettre à jour l'état de l'application.
Maintenant ouvert index.js importez le composant AppProvider et enveloppez le composant avec . Votre code final devrait ressembler à ce que j'ai ci-dessous.
import {AppProvider} de "./AppContext";
ReactDOM.render (
,
document.getElementById ("root")
L'emballage à l'intérieur de met AppContext à la disposition de chaque composant enfant de notre application.
N'oubliez pas qu'avec RTL, l'objectif est de tester notre application de la même manière qu'un réel l'utilisateur interagirait avec. Cela implique que nous voulons également que nos tests interagissent avec l'état de notre application. Pour cette raison, nous devons également mettre notre à la disposition de nos composants lors des tests. Voyons comment y arriver.
La méthode de rendu fournie par RTL est suffisante pour les composants simples qui n'ont pas besoin de maintenir l'état ou d'utiliser la navigation. Mais la plupart des applications nécessitent au moins l'un des deux. Pour cette raison, il propose une option wrapper . Avec ce wrapper, nous pouvons envelopper l'interface utilisateur rendue par le rendu de test avec n'importe quel composant que nous aimons, créant ainsi un rendu personnalisé . Créons-en un pour nos tests.
Créez un nouveau fichier src / custom-render.js et collez le code suivant.
import React from "react";
importer {render} depuis "@ testing-library / react";
importer {MemoryRouter} depuis "react-router-dom";
importer {AppProvider} à partir de "./AppContext";
const Wrapper = ({children}) => {
revenir (
{enfants}
);
};
const customRender = (ui, options) =>
render (ui, {wrapper: Wrapper, ... options});
// tout réexporter
export * de "@ testing-library / react";
// remplacer la méthode de rendu
export {customRender as render};
Ici, nous définissons un composant qui accepte certains composants enfants. Il enveloppe ensuite ces enfants à l'intérieur et . MemoryRouter est
Un qui conserve l'historique de votre "URL" en mémoire (ne lit ni n'écrit dans la barre d'adresse). Utile dans les tests et les environnements sans navigateur comme React Native .
Nous créons ensuite notre fonction de rendu, en lui fournissant le wrapper que nous venons de définir via son option wrapper. Cela a pour effet que tout composant que nous transmettons à la fonction de rendu est rendu à l'intérieur de ayant ainsi accès à la navigation et à l'état de notre application.
L'étape suivante consiste à tout exporter depuis @ testing-library / réagir . Enfin, nous exportons notre fonction de rendu personnalisé en tant que rendu remplaçant ainsi le rendu par défaut.
Notez que même si vous utilisiez Redux pour la gestion des états, le même modèle s'applique toujours.
Assurons-nous maintenant notre nouvelle fonction de rendu fonctionne. Importez-le dans src / App.test.js et utilisez-le pour rendre le composant .
Ouvrez App.test.js et remplacez la ligne d'importation. Cette
importation {render} de '@ testing-library / react';
devrait devenir
import {render} from './custom-render';[19659042[Letestréussit-iltoujours?Bontravail
Il y a un petit changement que je veux faire avant de terminer cette section. Il devient fatigant très rapidement de devoir écrire const {getByText} et d'autres requêtes à chaque fois. Donc, je vais désormais utiliser l'objet screen de la bibliothèque de tests DOM.
Importez l'objet screen depuis notre fichier de rendu personnalisé et remplacez le bloc decrire par le bloc code ci-dessous.
import {render, screen} from "./custom-render";
décrire ("", () => {
il ("Rendu composant correctement", () => {
rendu ();
attendre(
screen.getByText (/ Prise en main de la bibliothèque de tests React / i)
) .toBeInTheDocument ();
});
});
Nous accédons maintenant à la requête getByText à partir de l'objet écran. Votre test réussit-il toujours? Je suis sûr que oui. Continuons.
Si vos tests ne réussissent pas, vous voudrez peut-être comparer votre code avec le mien. La branche correspondante à ce stade est 02-setup-store-and-render .
Test et création de la page d'index de la liste des choses à faire
Dans cette section, nous allons tirer les choses à faire éléments de http://jsonplaceholder.typicode.com/ . Notre spécification de composant est très simple. Lorsqu'un utilisateur visite la page d'accueil de notre application,
affiche un indicateur de chargement qui indique Récupération des tâches en attendant la réponse de l'API;
affiche le titre de 15 tâches à faire sur l'écran une fois l'appel API renvoie (l'appel API renvoie 200). De plus, chaque titre d'élément doit être un lien qui mènera à la page des détails de la tâche.
Suivant une approche pilotée par les tests, nous écrirons notre test avant d'implémenter la logique du composant. Avant de faire cela, nous devons avoir le composant en question. Alors allez-y et créez un fichier src / TodoList.js et entrez le contenu suivant:
Puisque nous connaissons les spécifications des composants, nous pouvons les tester isolément avant de les intégrer à notre application principale. Je pense que c'est au développeur à ce stade de décider comment il veut gérer cela. L'une des raisons pour lesquelles vous voudrez peut-être tester un composant isolément est de ne pas interrompre accidentellement un test existant et de devoir ensuite combattre les incendies à deux endroits. Ceci étant terminé, écrivons maintenant le test.
Créez un nouveau fichier src / TodoList.test.js et entrez le code ci-dessous:
import React from "react";
importer des axios depuis "axios";
import {render, screen, waitForElementToBeRemoved} de "./custom-render";
importer {TodoList} à partir de "./TodoList";
importer {todos} de "./makeTodos";
décrire ("", () => {
it ("Composant Rendu ", async () => {
rendu ();
attendre waitForElementToBeRemoved (() => screen.getByText (/ Récupération de todos / i));
attendre (axios.get) .toHaveBeenCalledTimes (1);
todos.slice (0, 15) .forEach ((td) => {
attendez (screen.getByText (td.title)). toBeInTheDocument ();
});
});
});
À l'intérieur de notre bloc de test, nous rendons le composant et utilisons la fonction waitForElementToBeRemoved pour attendre que le texte Fetching todos disparaisse de l'écran. Une fois que cela se produit, nous savons que notre appel API est de retour. Nous vérifions également qu'un appel à Axios get a été tiré une fois. Enfin, nous vérifions que chaque titre de tâche est affiché à l'écran. Notez que le bloc il reçoit une fonction asynchrone . Cela est nécessaire pour que nous puissions utiliser attendre à l'intérieur de la fonction.
Chaque tâche à effectuer retournée par l'API a la structure suivante.
Nous voulons renvoyer un tableau de ces éléments lorsque nous
importons {todos} à partir de "./makeTodos"[19659042[Laseuleconditionestquechaque id soit unique.
Créez un nouveau fichier src / makeTodos.js et entrez le contenu ci-dessous. C'est la source des todos que nous utiliserons dans nos tests.
const makeTodos = (n) => {
// retourne n nombre de tâches
// la valeur par défaut est 15
const num = n || 15;
const todos = [];
pour (soit i = 0; i <num; i ++) {
todos.push ({
id: i,
userId: i,
title: `Todo item $ {i}`,
complété: [true, false][Math.floor(Math.random() * 2)],
});
}
return todos;
};
export const todos = makeTodos (200);
Cette fonction génère simplement une liste de n tâches à effectuer. La ligne terminée est définie en choisissant aléatoirement entre vrai et faux .
Les tests unitaires sont censés être rapides. Ils devraient fonctionner en quelques secondes. Échouez vite! C'est l'une des raisons pour lesquelles il est impossible de laisser nos tests effectuer des appels API réels. Pour éviter cela, nous nous moquons de tels appels d'API imprévisibles. Se moquer signifie simplement remplacer une fonction par une fausse version, nous permettant ainsi de personnaliser le comportement. Dans notre cas, nous voulons nous moquer de la méthode get d'Axios pour retourner tout ce que nous voulons. Jest fournit déjà une fonctionnalité de simulation hors de la boîte.
Faisons maintenant une simulation d'Axios afin qu'il retourne cette liste de tâches lorsque nous faisons l'appel d'API dans notre test. Créez un fichier src / __ mocks __ / axios.js et entrez le contenu ci-dessous:
import {todos} from "../makeTodos";
exporter par défaut {
get: jest.fn (). mockImplementation ((url) => {
switch (url) {
cas "https://jsonplaceholder.typicode.com/todos":
return Promise.resolve ({data: todos});
défaut:
jeter une nouvelle erreur (`URL UNMATCHED: $ {url}`);
}
}),
};
Notez que nous pouvons personnaliser les éléments à renvoyer en fonction de l'URL reçue par l'appel Axios. En outre, les appels Axios renvoient une promesse qui se résout aux données réelles que nous voulons, nous renvoyons donc une promesse avec les données que nous voulons.
À ce stade, nous avons un test réussi et un test échoué. Implémentons la logique du composant.
Ouvrez src / TodoList.js construisons l'implémentation pièce par pièce. Commencez par remplacer le code à l'intérieur par celui-ci ci-dessous:
import React from "react";
importer des axios depuis "axios";
importer {Link} à partir de "react-router-dom";
import "./App.css";
importer {AppContext} de "./AppContext";
export const TodoList = () => {
const [loading, setLoading] = React.useState (true);
const {appData, appDispatch} = React.useContext (AppContext);
React.useEffect (() => {
axios.get ("https://jsonplaceholder.typicode.com/todos") .then ((resp) => {
const {data} = resp;
appDispatch ({type: "LOAD_TODOLIST", todoList: data});
setLoading (false);
});
}, [appDispatch, setLoading]);
revenir (
// le bloc de code suivant va ici
);
};
Nous importons AppContext et déformons appData et appDispatch à partir de la valeur de retour de React.useContext . Nous effectuons ensuite l'appel d'API à l'intérieur d'un bloc useEffect . Une fois l'appel d'API renvoyé, nous définissons la liste des tâches à effectuer en lançant l'action LOAD_TODOLIST . Enfin, nous avons défini l'état de chargement sur false pour révéler nos tâches.
Nous découpons appData.todoList pour obtenir les 15 premiers éléments. Nous les cartographions ensuite et les rendons chacun dans une balise afin de pouvoir cliquer dessus et voir les détails. Notez l'attribut data-testid sur chaque lien. Cela devrait être un ID unique qui nous aidera à trouver des éléments DOM individuels. Dans le cas où nous avons un texte similaire à l'écran, nous ne devrions jamais avoir le même ID pour deux éléments. Nous verrons comment l'utiliser un peu plus tard.
Mes tests réussissent maintenant. Le vôtre passe-t-il? Super.
Incorporons maintenant ce composant dans notre arborescence de rendu. Ouvrez App.js faisons cela.
Tout d'abord. Ajoutez quelques importations.
import {BrowserRouter, Route} de "react-router-dom";
importez {TodoList} à partir de "./TodoList";=19659042. après l'élément .
Ceci indique simplement au navigateur de rendre le composant lorsque nous sommes à l'emplacement racine, / . Une fois que cela est fait, nos tests réussissent toujours, mais vous devriez voir des messages d'erreur sur votre console vous dire quelque chose agir . Vous devez également voir que le composant semble être le coupable ici.
Terminal affichant des avertissements d'acte. ( Grand aperçu )
Puisque nous sommes sûrs que notre composant TodoList en lui-même est correct, nous devons regarder le composant App, à l'intérieur duquel est rendu le composant .
Cet avertissement peut sembler complexe au premier abord, mais il nous indique qu'il se passe quelque chose dans notre composant que nous ne tenons pas compte dans notre test. Le correctif consiste à attendre que l'indicateur de chargement soit supprimé de l'écran avant de continuer.
Ouvrez App.test.js et mettez à jour le code pour qu'il ressemble à ceci:
import React from "réagir";
import {render, screen, waitForElementToBeRemoved} de "./custom-render";
importer l'application depuis "./App";
décrire ("", () => {
it ("Rends correctement le composant", async () => {
rendu ();
attendre(
screen.getByText (/ Prise en main de la bibliothèque de tests React / i)
) .toBeInTheDocument ();
attendre waitForElementToBeRemoved (() => screen.getByText (/ Récupération de todos / i));
});
});
Nous avons apporté deux modifications. Tout d'abord, nous avons changé la fonction du bloc il en une fonction asynchrone . C'est une étape nécessaire pour nous permettre d'utiliser attendre dans le corps de la fonction. Deuxièmement, nous attendons que le texte Fetching todos soit supprimé de l'écran. Et le tour est joué!. L'avertissement a disparu. Phew! Je vous conseille vivement de mettre en signet ce post de Kent Dodds pour en savoir plus sur cet avertissement act . Vous en aurez besoin.
Ouvrez maintenant la page dans votre navigateur et vous devriez voir la liste des tâches. Vous pouvez cliquer sur un élément si vous le souhaitez, mais il ne vous montrera rien car notre routeur ne reconnaît pas encore cette URL.
À titre de comparaison, la branche de mon dépôt à ce stade est 03 todolist .
Ajoutons maintenant la page des détails de la tâche.
Test et création de la page de tâche unique
Pour afficher un élément de tâche unique, nous suivrons une approche similaire. La spécification des composants est simple. Lorsqu'un utilisateur accède à une page de tâches:
affiche un indicateur de chargement qui indique Récupération de l'ID de l'élément de tâche où id représente l'ID de la tâche, tandis que l'API appelle https: / /jsonplaceholder.typicode.com/todos/item_id s'exécute.
Lorsque l'appel d'API revient, affichez les informations suivantes:
Titre de l'élément Todo
Ajouté par: userId [19659019] Cet élément est terminé si la tâche est terminée ou
Cet élément n'est pas encore terminé si la tâche n'est pas terminée.
Commençons par le composant. Créez un fichier src / TodoItem.js et ajoutez le contenu suivant:
import React from "react";
importer {useParams} à partir de "react-router-dom";
import "./App.css";
exportation const TodoItem = () => {
const {id} = useParams ()
revenir (
);
};
La seule chose nouvelle pour nous dans ce fichier est la ligne const {id} = useParams () . Il s'agit d'un crochet de react-router-dom qui nous permet de lire les paramètres d'URL. Cet identifiant va être utilisé pour récupérer un élément de tâche dans l'API.
Cette situation est un peu différente car nous allons lire l'identifiant à partir de l'URL de l'emplacement. Nous savons que lorsqu'un utilisateur clique sur un lien de tâches, l'ID s'affichera dans l'URL que nous pourrons ensuite récupérer à l'aide du crochet useParams () . Mais ici, nous testons le composant isolément, ce qui signifie qu'il n'y a rien à cliquer, même si nous le voulions. Pour contourner cela, nous devons nous moquer react-router-dom mais seulement certaines parties de celui-ci. Oui. Il est possible de se moquer uniquement de ce dont nous avons besoin. Voyons comment cela se passe.
Créez un nouveau fichier maquette src / __ mocks__ /react-router-dom.js. Collez maintenant le code suivant:
Vous devriez maintenant avoir remarqué que lorsque vous vous moquez d'un module, nous devons utiliser le nom exact du module comme nom de fichier factice.
Ici, nous utilisons la syntaxe module.exports parce que react-router-dom a principalement nommé exports. (Je n'ai rencontré aucune exportation par défaut depuis que j'y travaille. S'il y en a, veuillez la partager avec moi dans les commentaires). C'est différent d'Axios où tout est regroupé en tant que méthodes dans une seule exportation par défaut.
Nous avons d'abord diffusé le react-router-dom puis remplacé le hook useParams par une fonction Jest . Puisque cette fonction est une fonction Jest, nous pouvons la modifier à tout moment. Gardez à l'esprit que nous nous moquons uniquement de la partie dont nous avons besoin, car si nous nous moquons de tout, nous perdrons l'implémentation de MemoryHistory qui est utilisée dans notre fonction de rendu.
Commençons les tests! [19659005] Maintenant, créez src / TodoItem.test.js et entrez le contenu ci-dessous:
import React from "react";
importer des axios depuis "axios";
import {render, screen, waitForElementToBeRemoved} de "./custom-render";
importer {useParams, MemoryRouter} depuis "react-router-dom";
importer {TodoItem} de "./TodoItem";
décrire ("", () => {
it ("peut distinguer les fonctions non moquées", () => {
attend (jest.isMockFunction (useParams)). toBe (true);
attendez (jest.isMockFunction (MemoryRouter)). toBe (false);
});
});
Tout comme avant, nous avons toutes nos importations. Le bloc de description suit ensuite. Notre premier cas n'est là que pour démontrer que nous ne faisons que nous moquer de ce dont nous avons besoin. Jest's isMockFunction peut dire si une fonction est ou non fausse. Les deux attentes passent, confirmant le fait que nous avons une maquette là où nous la voulons.
Ajoutez le cas de test ci-dessous pour savoir quand une tâche est terminée.
il ("Rendu correctement pour un élément terminé", async () => {
useParams.mockReturnValue ({id: 1});
rendre ();
attendre waitForElementToBeRemoved (() =>
screen.getByText (/ Récupération de l'élément à faire 1 / i)
);
attendre (axios.get) .toHaveBeenCalledTimes (1);
attendez (screen.getByText (/ todo item 1 /)). toBeInTheDocument ();
attendez (screen.getByText (/ Ajouté par: 1 /)). toBeInTheDocument ();
attendre(
screen.getByText (/ Cet élément est terminé /)
) .toBeInTheDocument ();
});
La toute première chose que nous faisons est de se moquer de la valeur de retour de useParams . Nous voulons qu'il renvoie un objet avec une propriété id, ayant une valeur de 1. Lorsque cela est analysé dans le composant, nous nous retrouvons avec l'URL suivante https://jsonplaceholder.typicode.com/todos/1 . Gardez à l'esprit que nous devons ajouter un cas pour cette URL dans notre maquette Axios ou cela générera une erreur. Nous le ferons dans un instant.
Nous savons maintenant avec certitude que l'appel de useParams () renverra l'objet {id: 1} ce qui rend ce scénario de test prévisible.
Comme pour les tests précédents, nous attendons que l'indicateur de chargement, Récupération de la tâche 1 soit supprimé de l'écran avant de faire nos attentes. Nous nous attendons à voir le titre de la tâche, l'ID de l'utilisateur qui l'a ajouté et un message indiquant l'état.
Ouvrez src / __ mocks __ / axios.js et ajoutez le cas suivant à la interrupteur bloc.
Lorsque cette URL est mise en correspondance, une promesse avec une tâche terminée est renvoyée. Bien sûr, ce scénario de test échoue car nous n'avons pas encore implémenté la logique du composant. Allez-y et ajoutez un cas de test pour quand la tâche n'est pas terminée.
it ("Rendu correctement pour un élément incomplet", async () => {
useParams.mockReturnValue ({id: 2});
rendre ();
attendre waitForElementToBeRemoved (() =>
screen.getByText (/ Récupération de l'élément à faire 2 / i)
);
attendre (axios.get) .toHaveBeenCalledTimes (2);
attendez (screen.getByText (/ todo item 2 /)). toBeInTheDocument ();
attendez (screen.getByText (/ Ajouté par: 2 /)). toBeInTheDocument ();
attendre(
screen.getByText (/ Cet élément n'est pas encore terminé /)
) .toBeInTheDocument ();
});
C'est la même chose que dans le cas précédent. The only difference is the ID of the to-do, the userIdand the completion status. When we enter the component, we’ll need to make an API call to the URL https://jsonplaceholder.typicode.com/todos/2. Go ahead and add a matching case statement to the switch block of our Axios mock.
As with the component, we import AppContext. We read activeTodoItem from it, then we read the to-do title, userId, and completion status. After that we make the API call inside a useEffect block. When the API call returns we set the to-do in state by firing the LOAD_SINGLE_TODO action. Finally, we set our loading state to false to reveal the to-do details.
Let’s add the final piece of code inside the return div:
{loading ? (
Fetching todo item {id}
) : (
{title}
Added by: {userId}
{completed ? (
This item has been completed
) : (
This item is yet to be completed
)}
)}
Once this is done all tests should now pass. Yay! We have another winner.
Our component tests now pass. Mais nous ne l'avons toujours pas ajouté à notre application principale. Let’s do that.
Open src/App.js and add the import line:
import { TodoItem } from './TodoItem'
Add the TodoItem route above the TodoList route. Be sure to preserve the order shown below.
# preserve this order
Open your project in your browser and click on a to-do. Cela vous amène-t-il à la page des tâches? Bien sûr que oui. Good job.
In case you’re having any problem, you can check out my code at this point from the 04-test-todo branch.
Phew! Ça a été un marathon. Mais supporte-moi. Il y a un dernier point que je voudrais que nous abordions. Ayons rapidement un cas de test pour quand un utilisateur visite notre application, puis cliquez sur un lien à faire. Il s'agit d'un test fonctionnel pour imiter le fonctionnement de notre application. En pratique, c'est tout le test que nous devons faire pour cette application. It ticks every box in our app specification.
Open App.test.js and add a new test case. The code is a bit long so we’ll add it in two steps.
import userEvent from "@testing-library/user-event";
import { todos } from "./makeTodos";
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
}));
describe(""
...
// previous test case
...
it("Renders todos, and I can click to view a todo item", async () => {
render();
await waitForElementToBeRemoved(() => screen.getByText(/Fetching todos/i));
todos.slice(0, 15).forEach((td) => {
expect(screen.getByText(td.title)).toBeInTheDocument();
});
// click on a todo item and test the result
const { id, title, completed, userId } = todos[0];
axios.get.mockImplementationOnce(() =>
Promise.resolve({
data: { id, title, userId, completed },
})
);
userEvent.click(screen.getByTestId(String(id)));
await waitForElementToBeRemoved(() =>
screen.getByText(`Fetching todo item ${String(id)}`)
);
// next code block goes here
});
});
We have two imports of which userEvent is new. According to the docs,
“user-event is a companion library for the React Testing Library that provides a more advanced simulation of browser interactions than the built-in fireEvent method.”
Yes. There is a fireEvent method for simulating user events. But userEvent is what you want to be using henceforth.
Before we start the testing process, we need to restore the original useParams hooks. Ceci est nécessaire car nous voulons tester le comportement réel, nous devons donc nous moquer le moins possible. Jest provides us with requireActual method which returns the original react-router-dom module.
Note that we must do this before we enter the describe block, otherwise, Jest would ignore it. It states in the documentation that requireActual:
“...returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not.”
Once this is done, Jest bypasses every other check and ignores the mocked version of the react-router-dom.
As usual, we render the component and wait for the Fetching todos loading indicator to disappear from the screen. We then check for the presence of the first 15 to-do items on the page.
Once we’re satisfied with that, we grab the first item in our to-do list. To prevent any chance of a URL collision with our global Axios mock, we override the global mock with Jest’s mockImplementationOnce. Cette valeur simulée est valide pour un appel à la méthode get d'Axios. We then grab a link by its data-testid attribute and fire a user click event on that link. Then we wait for the loading indicator for the single to-do page to disappear from the screen.
Now finish the test by adding the below expectations in the position indicated.
expect(screen.getByText(title)).toBeInTheDocument();
expect(screen.getByText(`Added by: ${userId}`)).toBeInTheDocument();
switch (completed) {
case true:
expect(
screen.getByText(/This item has been completed/)
).toBeInTheDocument();
break;
case false:
expect(
screen.getByText(/This item is yet to be completed/)
).toBeInTheDocument();
break;
default:
throw new Error("No match");
}
We expect to see the to-do title and the user who added it. Enfin, comme nous ne pouvons pas être sûrs de l'état des tâches, nous créons un bloc de commutation pour gérer les deux cas. If a match is not found we throw an error.
You should have 6 passing tests and a functional app at this point. In case you’re having trouble, the corresponding branch in my repo is 05-test-user-action.
Conclusion
Phew! C'était un marathon. Si vous en êtes arrivé là, félicitations. Vous avez maintenant presque tout ce dont vous avez besoin pour écrire des tests pour vos applications React. I strongly advise that you read CRA’s testing docs and RTL’s documentation. Overall both are relatively short and direct.
I strongly encourage you to start writing tests for your React apps, no matter how small. Même si c'est juste des tests de fumée pour vous assurer que vos composants sont rendus. You can incrementally add more test cases over time.