Augmentez la maintenabilité du code avec les tests d'intégration React
Publié: 2022-03-11Les tests d'intégration sont un juste milieu entre le coût et la valeur des tests. L'écriture de tests d'intégration pour une application React à l'aide de react-testing-library au lieu ou en plus des tests unitaires des composants peut augmenter la maintenabilité du code sans nuire à la vitesse de développement.
Si vous souhaitez avoir une longueur d'avance avant de continuer, vous pouvez voir un exemple d'utilisation de la bibliothèque de tests de réaction pour les tests d'intégration de l'application React ici.
Pourquoi investir dans les tests d'intégration ?
"Les tests d'intégration établissent un excellent équilibre entre la confiance et la vitesse/les dépenses. C'est pourquoi il est conseillé d'y consacrer la majeure partie (pas la totalité, remarquez) de vos efforts."
– Kent C. Dodds dans les tests d'écriture. Pas trop. Surtout l'intégration.
Il est courant d'écrire des tests unitaires pour les composants React, en utilisant souvent une bibliothèque populaire pour tester les "enzymes" React ; plus précisément, sa méthode "peu profonde". Cette approche nous permet de tester les composants indépendamment du reste de l'application. Cependant, étant donné que l'écriture d'applications React consiste à composer des composants, les tests unitaires ne garantissent pas à eux seuls que l'application est exempte de bogues.
Par exemple, la modification des accessoires acceptés d'un composant et la mise à jour de ses tests unitaires associés peuvent entraîner la réussite de tous les tests alors que l'application peut toujours être interrompue si un autre composant n'a pas été mis à jour en conséquence.
Les tests d'intégration peuvent aider à préserver la tranquillité d'esprit tout en apportant des modifications à une application React, car ils garantissent que la composition des composants aboutit à l'UX souhaitée.
Exigences pour les tests d'intégration d'applications React
Voici quelques-unes des choses que les développeurs de React veulent faire lors de l'écriture de tests d'intégration :
- Testez les cas d'utilisation des applications du point de vue de l'utilisateur. Les utilisateurs accèdent aux informations sur une page Web et interagissent avec les commandes disponibles.
- Les appels d'API simulés ne dépendent pas de la disponibilité et de l'état de l'API pour réussir/échouer les tests.
- API de navigateur fictives (par exemple, stockage local) car elles n'existent tout simplement pas dans l'environnement de test.
- Assert sur l'état de React DOM (navigateur DOM ou environnement mobile natif).
Maintenant, pour certaines choses, nous devrions essayer d' éviter lors de l'écriture des tests d'intégration de l'application React :
- Testez les détails de la mise en œuvre. Les changements d'implémentation ne devraient interrompre un test que s'ils ont effectivement introduit un bogue.
- Se moquer trop. Nous voulons tester comment toutes les parties de l'application fonctionnent ensemble.
- Rendu peu profond. Nous voulons tester la composition de tous les composants de l'application jusqu'au plus petit composant.
Pourquoi choisir React-testing-library ?
Les exigences susmentionnées font de la bibliothèque de tests de réaction un excellent choix, car son principe directeur principal est de permettre aux composants React d'être testés d'une manière qui ressemble à la façon dont ils sont utilisés par un humain réel.
La bibliothèque, avec ses bibliothèques complémentaires facultatives, nous permet d'écrire des tests qui interagissent avec DOM et affirment son état.
Exemple de configuration d'application
L'application pour laquelle nous allons écrire des exemples de tests d'intégration implémente un scénario simple :
- L'utilisateur entre un nom d'utilisateur GitHub.
- L'application affiche une liste de référentiels publics associés au nom d'utilisateur saisi.
La manière dont la fonctionnalité ci-dessus est implémentée ne devrait pas être pertinente du point de vue des tests d'intégration. Cependant, pour rester proche des applications du monde réel, l'application suit les modèles React courants, d'où l'application :
- Est une application d'une seule page (SPA).
- Effectue des requêtes API.
- Possède une gestion globale de l'état.
- Prend en charge l'internationalisation.
- Utilise une bibliothèque de composants React.
Le code source de l'implémentation de l'application peut être trouvé ici.
Rédaction de tests d'intégration
Installation des dépendances
Avec fil :
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
Ou avec npm :
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
Création d'un fichier de suite de tests d'intégration
Nous allons créer un fichier nommé viewGitHubRepositoriesByUsername.spec.js
file dans le dossier ./test
de notre application. Jest le ramassera automatiquement.
Importation des dépendances dans le fichier de test
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
Configuration de la suite de tests
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
Remarques:
- Avant tous les tests, simulez l'API GitHub pour renvoyer une liste de référentiels lorsqu'elle est appelée avec un nom d'utilisateur spécifique.
- Après chaque test, nettoyez le test React DOM afin que chaque test démarre à partir d'un endroit propre.
- Les blocs de
describe
spécifient le cas d'utilisation du test d'intégration et les variations de flux. - Les variations de débit que nous testons sont :
- L'utilisateur saisit un nom d'utilisateur valide auquel sont associés des référentiels GitHub publics.
- L'utilisateur saisit un nom d'utilisateur valide qui n'a aucun référentiel GitHub public associé.
- L'utilisateur entre un nom d'utilisateur qui n'existe pas sur GitHub.
-
it
bloque l'utilisation du rappel asynchrone car le cas d'utilisation qu'ils testent contient une étape asynchrone.
Rédaction du premier test de flux
Tout d'abord, l'application doit être rendue.
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
La méthode de render
importée du module @testing-library/react
rend l'application dans le test React DOM et renvoie les requêtes DOM liées au conteneur d'application rendue. Ces requêtes sont utilisées pour localiser les éléments DOM avec lesquels interagir et affirmer.
Maintenant, comme première étape du flux testé, l'utilisateur se voit présenter un champ de nom d'utilisateur et y saisit une chaîne de nom d'utilisateur.

userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
L'assistant userEvent
du module importé @testing-library/user-event
a une méthode de type
qui imite le comportement de l'utilisateur lorsqu'il tape du texte dans un champ de texte. Il accepte deux paramètres : l'élément DOM qui accepte l'entrée et la chaîne saisie par l'utilisateur.
Les utilisateurs trouvent généralement les éléments DOM par le texte qui leur est associé. Dans le cas d'une entrée, il s'agit soit d'un texte d'étiquette, soit d'un texte d'espace réservé. La méthode de requête getByPlaceholderText
renvoyée précédemment par render
nous permet de trouver l'élément DOM par le texte de l'espace réservé.
Veuillez noter que puisque le texte lui-même est souvent susceptible de changer, il est préférable de ne pas se fier aux valeurs de localisation réelles et de configurer à la place le module de localisation pour renvoyer une clé d'élément de localisation comme valeur.
Par exemple, lorsque la localisation "en-US" renverrait normalement Enter GitHub username
comme valeur pour la clé userSelection.usernamePlaceholder
, dans les tests, nous voulons qu'elle renvoie userSelection.usernamePlaceholder
.
Lorsque l'utilisateur saisit du texte dans un champ, il doit voir la valeur du champ de texte mise à jour.
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
Ensuite dans le flux, l'utilisateur clique sur le bouton d'envoi et s'attend à voir la liste des référentiels.
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
La méthode userEvent.click
imite l'utilisateur qui clique sur un élément DOM, tandis que la requête getByText
trouve un élément DOM par le texte qu'il contient. Le modificateur le closest
garantit que nous sélectionnons l'élément du bon type.
Remarque : Dans les tests d'intégration, les étapes remplissent souvent à la fois les rôles act
et d' assert
. Par exemple, nous affirmons que l'utilisateur peut cliquer sur un bouton en cliquant dessus.
À l'étape précédente, nous avons affirmé que l'utilisateur voit la section de la liste des référentiels de l'application. Maintenant, nous devons affirmer que, puisque la récupération de la liste des référentiels à partir de GitHub peut prendre un certain temps, l'utilisateur voit une indication que la récupération est en cours. Nous voulons également nous assurer que l'application n'indique pas à l'utilisateur qu'aucun référentiel n'est associé au nom d'utilisateur saisi pendant que la liste des référentiels est toujours en cours de récupération.
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
Notez que le préfixe de requête getBy
est utilisé pour affirmer que l'élément DOM peut être trouvé et que le préfixe de requête queryBy
est utile pour l'assertion opposée. De plus, queryBy
ne renvoie pas d'erreur si aucun élément n'est trouvé.
Ensuite, nous voulons nous assurer que, finalement, l'application finit de récupérer les référentiels et les affiche à l'utilisateur.
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
La méthode asynchrone waitForElement
est utilisée pour attendre une mise à jour DOM qui rendra l'assertion fournie en tant que paramètre de méthode vraie. Dans ce cas, nous affirmons que l'application affiche le nom et la description de chaque référentiel renvoyé par l'API GitHub simulée.
Enfin, l'application ne devrait plus afficher d'indicateur indiquant que les référentiels sont en cours de récupération et ne devrait plus afficher de message d'erreur.
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
Notre test d'intégration React qui en résulte ressemble à ceci :
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
Tests de flux alternatifs
Lorsque l'utilisateur saisit un nom d'utilisateur GitHub sans référentiels publics associés, l'application affiche un message approprié.
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
Lorsque l'utilisateur saisit un nom d'utilisateur GitHub qui n'existe pas, l'application affiche un message d'erreur.
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
Pourquoi les tests d'intégration de React Rock
Les tests d'intégration offrent vraiment un point idéal pour les applications React. Ces tests aident à détecter les bogues et à utiliser l'approche TDD tout en ne nécessitant pas de maintenance lorsque l'implémentation change.
React-testing-library, présenté dans cet article, est un excellent outil pour écrire des tests d'intégration React, car il vous permet d'interagir avec l'application comme le fait l'utilisateur et de valider l'état et le comportement de l'application du point de vue de l'utilisateur.
Espérons que les exemples fournis ici vous aideront à commencer à écrire des tests d'intégration sur des projets React nouveaux et existants. L'exemple de code complet qui inclut l'implémentation de l'application est disponible sur mon GitHub.