Développement React.js piloté par les tests : Tests unitaires React.js avec Enzyme et Jest
Publié: 2022-03-11Selon Michael Feathers, tout morceau de code qui n'a pas de tests est considéré comme du code hérité. Par conséquent, l'un des meilleurs moyens d'éviter de créer du code hérité consiste à utiliser le développement piloté par les tests (TDD).
Bien qu'il existe de nombreux outils disponibles pour les tests unitaires JavaScript et React.js, dans cet article, nous utiliserons Jest et Enzyme pour créer un composant React.js avec des fonctionnalités de base à l'aide de TDD.
Pourquoi utiliser TDD pour créer un composant React.js ?
TDD apporte de nombreux avantages à votre code - l'un des avantages d'une couverture de test élevée est qu'il permet une refactorisation facile du code tout en gardant votre code propre et fonctionnel.
Si vous avez déjà créé un composant React.js, vous avez réalisé que le code peut croître très rapidement. Il se remplit de nombreuses conditions complexes causées par des déclarations liées aux changements d'état et aux appels de service.
Chaque composant dépourvu de tests unitaires a un code hérité qui devient difficile à maintenir. Nous pourrions ajouter des tests unitaires après avoir créé le code de production. Cependant, nous pouvons courir le risque de passer à côté de certains scénarios qui auraient dû être testés. En créant d'abord des tests, nous avons plus de chances de couvrir tous les scénarios logiques de notre composant, ce qui faciliterait la refactorisation et la maintenance.
Comment testons-nous unitairement un composant React.js ?
Il existe de nombreuses stratégies que nous pouvons utiliser pour tester un composant React.js :
- Nous pouvons vérifier qu'une fonction particulière dans les
props
a été appelée lorsqu'un certain événement est envoyé. - Nous pouvons également obtenir le résultat de la fonction de
render
en fonction de l'état actuel du composant et le faire correspondre à une disposition prédéfinie. - Nous pouvons même vérifier si le nombre d'enfants du composant correspond à une quantité attendue.
Afin d'utiliser ces stratégies, nous allons utiliser deux outils utiles pour travailler avec des tests dans React.js : Jest et Enzyme.
Utiliser Jest pour créer des tests unitaires
Jest est un framework de test open-source créé par Facebook qui a une excellente intégration avec React.js. Il comprend un outil de ligne de commande pour l'exécution de tests similaire à ce que proposent Jasmine et Mocha. Cela nous permet également de créer des fonctions fictives avec une configuration presque nulle et fournit un très bon ensemble de matchers qui rend les assertions plus faciles à lire.
De plus, il offre une fonctionnalité très intéressante appelée "test d'instantané", qui nous aide à vérifier et à vérifier le résultat du rendu des composants. Nous utiliserons le test d'instantané pour capturer l'arborescence d'un composant et l'enregistrer dans un fichier que nous pourrons utiliser pour le comparer à un arbre de rendu (ou tout ce que nous transmettrons à la fonction expect
comme premier argument.)
Utilisation d'enzyme pour monter des composants React.js
Enzyme fournit un mécanisme pour monter et parcourir les arborescences de composants React.js. Cela nous aidera à accéder à ses propres propriétés et états ainsi qu'à ses accessoires enfants afin d'exécuter nos assertions.
Enzyme offre deux fonctions de base pour le montage des composants : shallow
et mount
. La fonction shallow
charge en mémoire uniquement le composant racine tandis que mount
charge l'arborescence DOM complète.
Nous allons combiner Enzyme et Jest pour monter un composant React.js et exécuter des assertions dessus.
Configurer notre environnement
Vous pouvez jeter un œil à ce dépôt, qui a la configuration de base pour exécuter cet exemple.
Nous utilisons les versions suivantes :
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
Création du composant React.js à l'aide de TDD
La première étape consiste à créer un test défaillant qui tentera de restituer un composant React.js à l'aide de la fonction peu profonde de l'enzyme.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
Après avoir exécuté le test, nous obtenons l'erreur suivante :
ReferenceError: MyComponent is not defined.
Nous créons ensuite le composant fournissant la syntaxe de base pour faire passer le test.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
Dans l'étape suivante, nous nous assurerons que notre composant restitue une disposition d'interface utilisateur prédéfinie à l'aide de la fonction toMatchSnapshot
de Jest.
Après avoir appelé cette méthode, Jest crée automatiquement un fichier d'instantané appelé [testFileName].snap
, auquel est ajouté le dossier __snapshots__
.
Ce fichier représente la disposition de l'interface utilisateur que nous attendons de notre rendu de composants.
Cependant, étant donné que nous essayons de faire du TDD pur , nous devons d'abord créer ce fichier, puis appeler la fonction toMatchSnapshot
pour faire échouer le test.
Cela peut sembler un peu déroutant, étant donné que nous ne savons pas quel format Jest utilise pour représenter cette mise en page.
Vous pourriez être tenté d'exécuter d'abord la fonction toMatchSnapshot
et de voir le résultat dans le fichier d'instantané, et c'est une option valide. Cependant, si nous voulons vraiment utiliser du TDD pur , nous devons apprendre comment les fichiers d'instantanés sont structurés.
Le fichier d'instantané contient une disposition qui correspond au nom du test. Cela signifie que si notre test a cette forme :
desc("ComponentA" () => { it("should do something", () => { … } });
Nous devrions le préciser dans la section des exportations : Component A should do something 1
.
Vous pouvez en savoir plus sur les tests d'instantanés ici.
Donc, nous créons d'abord le fichier MyComponent.test.js.snap
.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
Ensuite, nous créons le test unitaire qui vérifiera que l'instantané correspond aux éléments enfants du composant.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
Nous pouvons considérer components.getElements
comme le résultat de la méthode render.
Nous passons ces éléments à la méthode expect
afin d'exécuter la vérification sur le fichier d'instantané.
Après avoir exécuté le test, nous obtenons l'erreur suivante :
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest nous dit que le résultat de component.getElements
ne correspond pas à l'instantané. Donc, nous faisons passer ce test en ajoutant l'élément d'entrée dans MyComponent
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
L'étape suivante consiste à ajouter des fonctionnalités à input
en exécutant une fonction lorsque sa valeur change. Pour ce faire, nous spécifions une fonction dans la prop onChange
.
Nous devons d'abord modifier l'instantané pour faire échouer le test.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
Un inconvénient de modifier d'abord l'instantané est que l'ordre des accessoires (ou attributs) est important.
Jest triera par ordre alphabétique les props reçus dans la fonction expect
avant de les vérifier par rapport à l'instantané. Donc, nous devrions les spécifier dans cet ordre.
Après avoir exécuté le test, nous obtenons l'erreur suivante :
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
Pour que ce test réussisse, nous pouvons simplement fournir une fonction vide à onChange
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
Ensuite, nous nous assurons que l'état du composant change après la distribution de l'événement onChange
.
Pour ce faire, nous créons un nouveau test unitaire qui va appeler la fonction onChange
dans l'entrée en passant un événement afin d'imiter un événement réel dans l'UI.
Ensuite, nous vérifions que l' état du composant contient une clé nommée input
.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
Nous obtenons maintenant l'erreur suivante.
Expected value to be defined, instead received undefined
Cela indique que le composant n'a pas de propriété dans l'état appelé input
.
Nous faisons passer le test en définissant cette entrée dans l'état du composant.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
Ensuite, nous devons nous assurer qu'une valeur est définie dans la nouvelle entrée d'état. Nous obtiendrons cette valeur à partir de l'événement.
Alors, créons un test qui s'assure que l'état contient cette valeur.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
Nous faisons enfin passer ce test en obtenant la valeur de l'événement et en la définissant comme valeur d'entrée.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
Après nous être assurés que tous les tests ont réussi, nous pouvons refactoriser notre code.
Nous pouvons extraire la fonction transmise dans la prop onChange
à une nouvelle fonction appelée updateState
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
Nous avons maintenant un simple composant React.js créé à l'aide de TDD.
Sommaire
Dans cet exemple, nous avons essayé d'utiliser TDD pur en suivant chaque étape en écrivant le moins de code possible pour échouer et réussir les tests.
Certaines étapes peuvent sembler inutiles et nous pouvons être tentés de les sauter. Cependant, chaque fois que nous sautons une étape, nous finirons par utiliser une version moins pure de TDD.
L'utilisation d'un processus TDD moins strict est également valable et peut très bien fonctionner.
Ma recommandation pour vous est d'éviter de sauter des étapes et de ne pas vous sentir mal si vous trouvez cela difficile. Le TDD n'est pas une technique facile à maîtriser, mais cela en vaut vraiment la peine.
Si vous souhaitez en savoir plus sur le TDD et le développement piloté par le comportement (BDD) associé, lisez Your Boss Won't Appreciate TDD par son collègue Toptaler Ryan Wilcox.