Catégories
Plugin et site web

Comment tester vos applications React avec la bibliothèque de tests React – Smashing Magazine

A 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…
Plus à propos
Chidi

Les tests donnent confiance dans le 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 de React peuvent rapidement commencer à tester leur application avec la React Testing Library (RTL).

Aujourd'hui, nous allons brièvement expliquer pourquoi il est important d'écrire des tests automatisés pour tout projet logiciel, et mettre en lumière certains des types de tests automatisés courants. 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 d'Axios et de React-Router sont également requises.

Meilleures pratiques avec React

React est une fantastique bibliothèque JavaScript 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 devriez-vous 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 test.

Ce type de test nous donne l'avantage d'être prévisible et rapide, de sorte que nous pouvons rapidement trouver et corriger les bogues dans notre code.

Ayant vu la nécessité de tester notre code, la prochaine question logique est, quel type 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 de nombreux 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.

  1. Test de l'unité
    Dans ce type de test, l'objectif est de 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.
  2. 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. Si elle s'affiche correctement, nous pouvons être assez certains que notre application s'affichera sur le navigateur.
  3. 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.
  4. 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.
  5. 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 cypress pour les tests E2E.
  6. Test d'admission
    Cette opération est généralement effectuée par le propriétaire de l'entreprise pour vérifier que le système répond aux spécifications.
  7. Test de performance
    Ce type de test est effectué 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.

Il y en a plus ici si cela vous intéresse.

Pourquoi utiliser la bibliothèque de tests React?

En ce qui concerne le test des applications React, il existe quelques options de test disponibles, dont les plus courantes que je connaisse sont Enzyme and 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 principe directeur principal 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.»

Donc, quoi que vous fassiez, pensez à l'utilisateur final et testez votre application comme il l'utiliserait.

Choisir 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és. 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 de mise en œuvre.

Avec cela à l'écart, commençons par créer une application de liste de tâches, en suivant l'approche TDD.

Configuration du projet

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. Avec le projet en cours d'exécution, ouvrez un terminal séparé, exécutez yarn test puis appuyez sur a. Cela exécute tous les tests du projet dans watch mode. 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:

Réussite du test initial
Test initial réussi. (Grand aperçu)

Vous devriez voir beaucoup de verts, ce qui indique que le test que nous effectuons a réussi avec brio.

Comme je l'ai mentionné plus tôt, l'ARC met en place 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 le 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 faire une recherche à l'intérieur yarn.lock pour le trouver. Vous pouvez également le voir dans node_modules/.

Jest est incroyable dans la gamme de fonctionnalités qu'il offre. 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 prochaines sections.

Ouvert package.json voyons ce que nous avons là-bas. La section d'intérêt est dependencies.

  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    ...
  },

Nous avons les packages suivants installés spécifiquement à des fins de test:

  1. @ testing-library / jest-dom: fournit des comparateurs d'éléments DOM personnalisés pour Jest.
  2. @ testing-library / react: fournit les API pour tester les applications React.
  3. @ testing-library / user-event: fournit une simulation avancée des interactions du navigateur.

S'ouvrir App.test.js examinons son contenu.

import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  const { getByText } = render();
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

le render méthode de RTL rend la composant et renvoie un objet qui est déstructuré pour le getByText requete. 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 se trouve 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 correspond à l'expression régulière /learn react/i. le i flag rend l'expression régulière insensible à la casse. nous expect trouver le texte Learn React dans le document.

Tout cela imite le comportement d'un utilisateur dans le navigateur lors de l'interaction avec notre application.

Commençons par apporter les modifications requises par notre application. Ouvert App.js et remplacez le contenu par le code ci-dessous.

import React from "react";
import "./App.css";
function App() {
  return (
    

Getting started with React testing library

); } export default App;

Si le test est toujours en cours d'exécution, vous devriez voir l'échec du test. 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 avec le code ci-dessous:

# use describe, it pattern
describe("", () => {
  it("Renders  component correctly", () => {
    const { getByText } = render();
    expect(getByText(/Getting started with React testing library/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 describe et it modèle car il me permet de structurer mon fichier de test en blocs logiques de tests connexes. 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 learn react texte avec Getting started with React testing library.

Si vous n'avez pas le temps d'écrire vos propres styles, vous pouvez simplement copier celui ci-dessous dans App.css.

.App {
  min-height: 100vh;
  text-align: center;
}
.App-header {
  height: 10vh;
  display: flex;
  background-color: #282c34;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
.App-body {
  width: 60%;
  margin: 20px auto;
}
ul {
  padding: 0;
  display: flex;
  list-style-type: decimal;
  flex-direction: column;
}
li {
  font-size: large;
  text-align: left;
  padding: 0.5rem 0;
}
li a {
  text-transform: capitalize;
  text-decoration: none;
}
.todo-title {
  text-transform: capitalize;
}
.completed {
  color: green;
}
.not-completed {
  color: red;
}

Vous devriez déjà voir le titre de la page monter après l'ajout de 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 la configuration de notre 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 and 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 useContext crochet. Configurons donc le contexte de notre application.

Créer un nouveau fichier src/AppContext.js et entrez le contenu ci-dessous.

import React from "react";
export const AppContext = React.createContext({});

export const AppProvider = ({ children }) => {
  const reducer = (state, action) => {
    switch (action.type) {
      case "LOAD_TODOLIST":
        return { ...state, todoList: action.todoList };
      case "LOAD_SINGLE_TODO":
        return { ...state, activeToDoItem: action.todo };
      default:
        return state;
    }
  };
  const (appData, appDispatch) = React.useReducer(reducer, {
    todoList: (),
    activeToDoItem: { id: 0 },
  });
  return (
    
      {children}
    
  );
};

Ici, nous créons un nouveau contexte avec React.createContext({}), pour lequel la valeur initiale est un objet vide. Nous définissons ensuite un AppProvider composant qui accepte children composant. Il enveloppe ensuite ces enfants dans AppContext.Provider, rendant ainsi le { appData, appDispatch } objet disponible pour tous les enfants n'importe où dans l'arborescence de rendu.

Notre reducer La fonction définit deux types d'actions.

  1. LOAD_TODOLIST qui est utilisé pour mettre à jour le todoList tableau.
  2. LOAD_SINGLE_TODO qui est utilisé pour mettre à jour activeToDoItem.

appData et appDispatch sont tous deux retournés du useReducer crochet. appData nous donne accès aux valeurs en l'état tout en appDispatch nous donne une fonction que nous pouvons utiliser pour mettre à jour l'état de l'application.

Ouvert index.js, importez le AppProvider composant et envelopper le composant avec . Votre code final devrait ressembler à ce que j'ai ci-dessous.

import { AppProvider } from "./AppContext";

ReactDOM.render(
  
    
      
    
  ,
  document.getElementById("root")
);

Emballage à l'intérieur fait du AppContext disponible pour chaque composant enfant dans notre application.

N'oubliez pas qu'avec RTL, l'objectif est de tester notre application de la même manière qu'un véritable 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 à 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 fournit un wrapper option. 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éer un nouveau fichier src/custom-render.js et collez le code suivant.

import React from "react";
import { render } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";

import { AppProvider } from "./AppContext";

const Wrapper = ({ children }) => {
  return (
    
      {children}
    
  );
};

const customRender = (ui, options) =>
  render(ui, { wrapper: Wrapper, ...options });

// re-export everything
export * from "@testing-library/react";

// override render method
export { customRender as render };

Nous définissons ici un composant qui accepte certains composants enfants. Il enveloppe ensuite ces enfants à l'intérieur et . MemoryRouter est

UNE 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. L'effet de ceci est que tout composant que nous passons à la fonction de rendu est rendu à l'intérieur , ayant ainsi accès à la navigation et à l'état de notre application.

La prochaine étape consiste à tout exporter de @testing-library/react. Enfin, nous exportons notre fonction de rendu personnalisé sous render, remplaçant ainsi le rendu par défaut.

Notez que même si vous utilisiez Redux pour la gestion des états, le même schéma s'applique toujours.

Assurons-nous maintenant que notre nouvelle fonction de rendu fonctionne. Importez-le dans src/App.test.js et l'utiliser pour rendre le composant.

Ouvert App.test.js et remplacez la ligne d'importation. Ce

import { render } from '@testing-library/react';

devrait devenir

import { render } from './custom-render';

Le test réussit-il toujours? Bon travail.

Il y a un petit changement que je veux faire avant de terminer cette section. Ça devient fatigant très vite de devoir écrire const { getByText } et d'autres requêtes à chaque fois. Donc, je vais utiliser le screen objet de la bibliothèque de tests DOM désormais.

Importez l'objet écran à partir de notre fichier de rendu personnalisé et remplacez le describe bloquer avec le code ci-dessous.

import { render, screen } from "./custom-render";

describe("", () => {
  it("Renders  component correctly", () => {
    render();
    expect(
      screen.getByText(/Getting started with React testing library/i)
    ).toBeInTheDocument();
  });
});

Nous accédons maintenant à la getByText requête à partir de l'objet écran. Votre test réussit-il toujours? Je suis sûr que oui. Nous allons continuer.

Si vos tests ne réussissent pas, vous pouvez 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 tâches

Dans cette section, nous allons extraire les tâches à effectuer 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,

  1. afficher un indicateur de chargement qui dit Fetching todos en attendant la réponse de l'API;
  2. afficher le titre de 15 tâches à faire à l'écran une fois l'appel API renvoyé (l'appel API renvoie 200). De plus, chaque titre d'article devrait être un lien qui mènera à la page des détails de la tâche.

En suivant une approche pilotée par les tests, nous rédigerons notre test avant d'implémenter la logique des composants. 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:

import React from "react";
import "./App.css";
export const TodoList = () => {
  return (
    
); };

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. Avec cela à l'écart, écrivons maintenant le test.

Créer un nouveau fichier src/TodoList.test.js et entrez le code ci-dessous:

import React from "react";
import axios from "axios";
import { render, screen, waitForElementToBeRemoved } from "./custom-render";
import { TodoList } from "./TodoList";
import { todos } from "./makeTodos";

describe("", () => {
  it("Renders  component", async () => {
    render();
    await waitForElementToBeRemoved(() => screen.getByText(/Fetching todos/i));

    expect(axios.get).toHaveBeenCalledTimes(1);
    todos.slice(0, 15).forEach((td) => {
      expect(screen.getByText(td.title)).toBeInTheDocument();
    });
  });
});

À l'intérieur de notre bloc de test, nous rendons le composant et utiliser le waitForElementToBeRemoved fonction d'attendre la Fetching todos texte à disparaître de l'écran. Une fois que cela se produit, nous savons que notre appel API est de retour. Nous vérifions également qu'un Axios get l'appel a été tiré une fois. Enfin, nous vérifions que chaque titre de tâche est affiché à l'écran. Notez que le it bloc reçoit un async fonction. Cela est nécessaire pour que nous puissions utiliser await à l'intérieur de la fonction.

Chaque tâche à effectuer retournée par l'API a la structure suivante.

{
  id: 0,
  userId: 0,
  title: 'Some title',
  completed: true,
}

Nous voulons retourner un tableau de ces derniers lorsque nous

import { todos } from "./makeTodos"

La seule condition est que chaque id devrait être unique.

Créer 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) => {
  // returns n number of todo items
  // default is 15
  const num = n || 15;
  const todos = ();
  for (let i = 0; i < num; i++) {
    todos.push({
      id: i,
      userId: i,
      title: `Todo item ${i}`,
      completed: (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 à faire. le completed la ligne est définie en choisissant aléatoirement entre true et false.

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 moquer ces appels 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 moquerie prête à l'emploi.

Supposons maintenant Axios pour qu'il renvoie cette liste de tâches lorsque nous effectuons l'appel d'API dans notre test. Créer un fichier src/__mocks__/axios.js et entrez le contenu ci-dessous:

import { todos } from "../makeTodos";

export default {
  get: jest.fn().mockImplementation((url) => {
    switch (url) {
      case "https://jsonplaceholder.typicode.com/todos":
        return Promise.resolve({ data: todos });
      default:
        throw new Error(`UNMATCHED URL: ${url}`);
    }
  }),
};

Lorsque le test démarre, Jest trouve automatiquement ceci moqueurs dossier et au lieu d'utiliser le véritable Axios de node_modules/ dans nos tests, il utilise celui-ci. À ce stade, nous nous moquons seulement de la get utilisant la méthode mockImplementation de Jest. De même, nous pouvons nous moquer d'autres méthodes Axios comme post, patch, interceptors, defaults etc. À l'heure actuelle, ils ne sont pas tous définis et toute tentative d'accès, axios.post par exemple, entraînerait une erreur.

Notez que nous pouvons personnaliser les éléments à renvoyer en fonction de l'URL que l'appel Axios reçoit. De plus, 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.

Ouvert 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";
import axios from "axios";
import { Link } from "react-router-dom";
import "./App.css";
import { AppContext } from "./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));

  return (
    
// next code block goes here
); };

Nous importons AppContext et déstructurer appData et appDispatch de la valeur de retour de React.useContext. Nous faisons ensuite l'appel d'API à l'intérieur d'un useEffect bloquer. Une fois l'appel de l'API renvoyé, nous définissons la liste des tâches à effectuer en tirant le LOAD_TODOLIST action. Enfin, nous avons défini l'état de chargement sur false pour révéler nos tâches.

Entrez maintenant le dernier morceau de code.

{loading ? (
  

Fetching todos

) : (
    {appData.todoList.slice(0, 15).map((item) => { const { id, title } = item; return (
  • {title}
  • ); })}
)}

Nous coupons appData.todoList pour obtenir les 15 premiers articles. Nous cartographions ensuite ceux-ci et les rendons chacun dans un tag afin que nous puissions cliquer dessus et voir les détails. Noter la data-testid attribut 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? Génial.

Intégrons maintenant ce composant dans notre arborescence de rendu. S'ouvrir App.js faisons cela.

Premières choses. Ajoutez quelques importations.

import { BrowserRouter, Route } from "react-router-dom";
import { TodoList } from "./TodoList";

Nous avons besoin BrowserRouter pour la navigation et Route pour rendre chaque composant dans chaque emplacement de navigation.

Maintenant, ajoutez le code ci-dessous après le

élément.

C'est simplement dire au navigateur de rendre le composant lorsque nous sommes sur l'emplacement racine, /. Une fois cela fait, nos tests réussissent toujours, mais vous devriez voir des messages d'erreur sur votre console vous en informer. act quelque chose. Vous devez également voir que le composant semble être le coupable ici.

Terminal affichant des avertissements d'acte
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.

S'ouvrir App.test.js et mettez à jour le code pour qu'il ressemble à ceci:

import React from "react";
import { render, screen, waitForElementToBeRemoved } from "./custom-render";
import App from "./App";
describe("", () => {
  it("Renders  component correctly", async () => {
    render();
    expect(
      screen.getByText(/Getting started with React testing library/i)
    ).toBeInTheDocument();
    await waitForElementToBeRemoved(() => screen.getByText(/Fetching todos/i));
  });
});

Nous avons apporté deux modifications. Tout d'abord, nous avons changé la fonction dans le it bloquer à un async fonction. Il s'agit d'une étape nécessaire pour nous permettre d'utiliser await dans le corps de fonction. Deuxièmement, nous attendons la Fetching todos texte à supprimer de l'écran. Et le tour est joué!. L'avertissement a disparu. Phew! Je vous conseille fortement de mettre cet article en signet de Kent Dodds pour en savoir plus act avertissement. Tu vas en avoir 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 repo à ce stade est 03-todolist.

Ajoutons maintenant la page des détails de la tâche.

Test et création de la page unique

Pour afficher un seul élément à faire, nous suivrons une approche similaire. La spécification des composants est simple. Lorsqu'un utilisateur accède à une page de tâches:

  1. afficher un indicateur de chargement qui dit Fetching todo item id où id représente l'id de la tâche, tandis que l'appel d'API à https://jsonplaceholder.typicode.com/todos/item_id s'exécute.
  2. Lorsque l'appel d'API revient, affichez les informations suivantes:
    • Titre de l'article
    • Ajouté par: userId
    • Cet élément est terminé si la tâche est terminée ou
    • Cet article n'est pas encore terminé si la tâche n'est pas terminée.

Commençons par le composant. Créer un fichier src/TodoItem.js et ajoutez le contenu suivant.

import React from "react";
import { useParams } from "react-router-dom";

import "./App.css";

export const TodoItem = () => {
  const { id } = useParams()
  return (
    
); };

La seule chose nouvelle pour nous dans ce fichier est la const { id } = useParams() ligne. Ceci est 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'ID à partir de l'URL de localisation. Nous savons que lorsqu'un utilisateur clique sur un lien de tâches, l'identifiant s'affiche dans l'URL que nous pouvons ensuite saisir à l'aide du useParams() crochet. 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 fait.

Créer un nouveau fichier maquette src/__mocks__ /react-router-dom.js. Collez maintenant le code suivant:

module.exports = {
  ...jest.requireActual("react-router-dom"),
  useParams: jest.fn(),
};

À présent, vous devriez avoir remarqué que lorsque vous vous moquez d'un module, nous devons utiliser le nom exact du module comme nom de fichier fictif.

Ici, nous utilisons le module.exports syntaxe car react-router-dom a surtout nommé les exportations. (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 réparti le réel react-router-dom, puis remplacez le useParams crochet avec une fonction Jest. Puisque cette fonction est une fonction Jest, nous pouvons la modifier à tout moment. Gardez à l'esprit que nous ne nous moquons que de la partie dont nous avons besoin parce que si nous nous moquons de tout, nous perdrons l'implémentation de MemoryHistory qui est utilisé dans notre fonction de rendu.

Commençons les tests!

Maintenant, créez src/TodoItem.test.js et entrez le contenu ci-dessous:

import React from "react";
import axios from "axios";
import { render, screen, waitForElementToBeRemoved } from "./custom-render";
import { useParams, MemoryRouter } from "react-router-dom";
import { TodoItem } from "./TodoItem";

describe("", () => {
  it("can tell mocked from unmocked functions", () => {
    expect(jest.isMockFunction(useParams)).toBe(true);
    expect(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. La fonction isMockFunction de Jest peut indiquer si une fonction est ou non fictive. Les deux attentes passent, confirmant le fait que nous avons une maquette là où nous la voulons.

Ajoutez le scénario de test ci-dessous pour savoir quand une tâche est terminée.

  it("Renders  correctly for a completed item", async () => {
    useParams.mockReturnValue({ id: 1 });
    render();

    await waitForElementToBeRemoved(() =>
      screen.getByText(/Fetching todo item 1/i)
    );

    expect(axios.get).toHaveBeenCalledTimes(1);
    expect(screen.getByText(/todo item 1/)).toBeInTheDocument();
    expect(screen.getByText(/Added by: 1/)).toBeInTheDocument();
    expect(
      screen.getByText(/This item has been completed/)
    ).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 useParams() retournera l'objet { id: 1 } ce qui rend ce cas de test prévisible.

Comme pour les tests précédents, nous attendons l'indicateur de chargement, Fetching todo item 1 à supprimer 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.

Ouvert src/__mocks__/axios.js et ajoutez le cas suivant au switch bloquer.

      case "https://jsonplaceholder.typicode.com/todos/1":
        return Promise.resolve({
          data: { id: 1, title: "todo item 1", userId: 1, completed: true },
        });

Lorsque cette URL correspond, une promesse avec une tâche terminée est retourné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("Renders  correctly for an uncompleted item", async () => {
    useParams.mockReturnValue({ id: 2 });
    render();
    await waitForElementToBeRemoved(() =>
      screen.getByText(/Fetching todo item 2/i)
    );
    expect(axios.get).toHaveBeenCalledTimes(2);
    expect(screen.getByText(/todo item 2/)).toBeInTheDocument();
    expect(screen.getByText(/Added by: 2/)).toBeInTheDocument();
    expect(
      screen.getByText(/This item is yet to be completed/)
    ).toBeInTheDocument();
  });

C'est la même chose que le cas précédent. La seule différence est l'ID de la tâche, le userIdet l'état d'achèvement. Lorsque nous entrons dans le composant, nous devons effectuer un appel API à l'URL https://jsonplaceholder.typicode.com/todos/2. Allez-y et ajoutez une déclaration de cas correspondant au bloc de commutation de notre maquette Axios.

case "https://jsonplaceholder.typicode.com/todos/2":
  return Promise.resolve({
    data: { id: 2, title: "todo item 2", userId: 2, completed: false },
  });

Lorsque l'URL correspond, une promesse avec une tâche inachevée est renvoyée.

Les deux cas de test échouent. Ajoutons maintenant l'implémentation des composants pour les faire passer.

Ouvert src/TodoItem.js et mettez à jour le code comme suit:

import React from "react";
import axios from "axios";
import { useParams } from "react-router-dom";
import "./App.css";
import { AppContext } from "./AppContext";

export const TodoItem = () => {
  const { id } = useParams();
  const (loading, setLoading) = React.useState(true);
  const {
    appData: { activeToDoItem },
    appDispatch,
  } = React.useContext(AppContext);

  const { title, completed, userId } = activeToDoItem;
  React.useEffect(() => {
    axios
      .get(`https://jsonplaceholder.typicode.com/todos/${id}`)
      .then((resp) => {
        const { data } = resp;
        appDispatch({ type: "LOAD_SINGLE_TODO", todo: data });
        setLoading(false);
      });
  }, (id, appDispatch));
  return (
    
// next code block goes here.
); };

Comme avec le composant, nous importons AppContext. Nous lisons activeTodoItem à partir de là, nous lisons le titre de la tâche, l'ID utilisateur et l'état d'achèvement. Après cela, nous faisons l'appel API à l'intérieur d'un useEffect bloquer. Lorsque l'appel de l'API revient, nous définissons l'état de la tâche en tirant le LOAD_SINGLE_TODO action. Enfin, nous avons défini notre état de chargement sur false pour révéler les détails de la tâche.

Ajoutons le dernier morceau de code à l'intérieur de la div de retour:

{loading ? (
  

Fetching todo item {id}

) : (

{title}

Added by: {userId}

{completed ? (

This item has been completed

) : (

This item is yet to be completed

)}
)}

Une fois cela fait, tous les tests devraient maintenant réussir. Yay! Nous avons un autre gagnant.

Nos tests de composants réussissent maintenant. Mais nous ne l'avons toujours pas ajouté à notre application principale. Faisons cela.

Ouvert src/App.js et ajoutez la ligne d'importation:

import { TodoItem } from './TodoItem'

Ajoutez l'itinéraire TodoItem au-dessus de l'itinéraire TodoList. Assurez-vous de conserver l'ordre indiqué ci-dessous.

# preserve this order

Ouvrez votre projet dans votre navigateur et cliquez sur une tâche. Cela vous amène-t-il à la page des tâches? Bien sûr que oui. Bon travail.

En cas de problème, vous pouvez consulter mon code à ce stade dans la branche 04-test-todo.

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. Il coche toutes les cases de nos spécifications d'application.

Ouvert App.test.js et ajoutez un nouveau cas de test. Le code est un peu long, nous allons donc l'ajouter en deux étapes.

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
  });
});

Nous avons deux importations dont userEvent est nouveau. Selon les documents,

"user-event est une bibliothèque complémentaire pour le React Testing Library qui fournit une simulation plus avancée des interactions du navigateur que le intégré fireEvent méthode."

Oui. Il y a un fireEvent méthode de simulation des événements utilisateur. Mais userEvent est ce que vous souhaitez désormais utiliser.

Avant de commencer le processus de test, nous devons restaurer l'original useParams crochets. Ceci est nécessaire car nous voulons tester le comportement réel, nous devons donc nous moquer le moins possible. Jest nous fournit la méthode requireActual qui renvoie l'original react-router-dom module.

Notez que nous devons le faire avant d'entrer dans le bloc de description, sinon Jest l'ignorerait. Il indique dans la documentation que requireActual:

"... renvoie le module réel au lieu d'une maquette, en contournant toutes les vérifications pour savoir si le module doit recevoir une implémentation factice ou non."

Une fois cela fait, Jest contourne toutes les autres vérifications et ignore la version moquée du react-router-dom.

Comme d'habitude, nous rendons le composant et attendre la Fetching todos indicateur de chargement pour disparaître de l'écran. Nous vérifions ensuite la présence des 15 premières choses à faire sur la page.

Une fois que nous en sommes satisfaits, nous récupérons le premier élément de notre liste de tâches. Pour éviter tout risque de collision d'URL avec notre maquette globale Axios, nous remplaçons la simulation globale par la simulation mockImplementationOnce de Jest. Cette valeur simulée est valide pour un appel à la méthode get d'Axios. Nous prenons ensuite un lien par son data-testid attribuer et déclencher un événement de clic utilisateur sur ce lien. Ensuite, nous attendons que l'indicateur de chargement pour que la page de tâche unique disparaisse de l'écran.

Terminez maintenant le test en ajoutant les attentes ci-dessous dans la position indiquée.

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");
    }
  

Nous nous attendons à voir le titre de la tâche et l'utilisateur qui l'a ajouté. 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. Si aucune correspondance n'est trouvée, nous lançons une erreur.

Vous devriez avoir 6 tests réussis et une application fonctionnelle à ce stade. En cas de problème, la branche correspondante dans mon référentiel est 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. Je vous conseille vivement de lire les documents de test de l'ARC et la documentation de RTL. Globalement, les deux sont relativement courts et directs.

Je vous encourage fortement à commencer à écrire des tests pour vos applications React, aussi petites soient-elles. Même si c'est juste des tests de fumée pour vous assurer que vos composants sont rendus. Vous pouvez ajouter progressivement plus de cas de test au fil du temps.

  • «Aperçu des tests», site officiel de React
  • "Expect, "Référence de l'API Jest
  • «Rendu personnalisé», bibliothèque de tests React
  • "jest-dom», Bibliothèque de tests, GitHub
  • «Principes directeurs», Mise en route, Bibliothèque de tests
  • «React Testing Library», Bibliothèque de tests
  • «Outils recommandés», Aperçu des tests, site officiel de React
  • «Correction de l'avertissement« non enveloppé dans l'acte (…) »», Kent C. Dodds
  • "», React Training
  • "screen, "Bibliothèque de tests DOM
  • "user-event», Écosystème, Test des documents de bibliothèque
  • «Les différents types de tests de logiciels», Sten Pittet, Atlassian
Smashing Editorial(ks, ra, yk, il)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *