Comment les composants React facilitent les tests d'interface utilisateur

Publié: 2022-03-11

Tester les back-ends est facile. Vous prenez le langage de votre choix, l'associez à votre framework préféré, écrivez des tests et cliquez sur "Exécuter". Votre console dit "Yay ! Ça marche!" Votre service d'intégration continue exécute vos tests à chaque poussée, la vie est belle.

Bien sûr, le développement piloté par les tests (TDD) est bizarre au début, mais un environnement prévisible, plusieurs exécuteurs de test, des outils de test intégrés dans des frameworks et une prise en charge de l'intégration continue facilitent la vie. Il y a cinq ans, je pensais que les tests étaient la solution à tous les problèmes que j'ai rencontrés.

Puis Backbone est devenu grand.

Nous sommes tous passés au MVC frontal. Nos backends testables sont devenus des serveurs de base de données glorifiés. Notre code le plus compliqué a été transféré dans le navigateur. Et nos applications n'étaient plus testables dans la pratique.

C'est parce que tester le code frontal et les composants de l'interface utilisateur est un peu difficile.

Ce n'est pas si mal si tout ce que nous voulons, c'est vérifier que nos modèles se comportent bien. Ou, que l'appel d'une fonction changera la bonne valeur. Tout ce que nous devons faire pour les tests unitaires React est :

  • Écrivez des modules bien formés et isolés.
  • Utilisez les tests Jasmine ou Mocha (ou autre) pour exécuter des fonctions.
  • Utilisez un testeur, comme Karma ou Chutzpah.

C'est ça. Notre code est testé unitairement.

Auparavant, exécuter des tests frontaux était la partie la plus difficile. Chaque framework avait ses propres idées et dans la plupart des cas, vous vous retrouviez avec une fenêtre de navigateur que vous actualisiez manuellement chaque fois que vous vouliez exécuter les tests. Bien sûr, vous oublieriez toujours. Au moins, je sais que je l'ai fait.

En 2012, Vojta Jina a sorti le coureur Karma (appelé Testacular à l'époque). Avec Karma, le test frontal devient un citoyen à part entière de la chaîne d'outils. Nos tests React s'exécutent dans un terminal ou sur un serveur d'intégration continue, ils se relancent lorsque nous modifions un fichier, et nous pouvons même tester notre code dans plusieurs navigateurs en même temps.

Que pourrions-nous souhaiter de plus ? Eh bien, pour tester réellement notre code frontal.

Les tests frontaux nécessitent plus que de simples tests unitaires

Les tests unitaires sont formidables : c'est le meilleur moyen de voir si un algorithme fait la bonne chose à chaque fois, ou de vérifier notre logique de validation des entrées, ou les transformations de données, ou toute autre opération isolée. Les tests unitaires sont parfaits pour les fondamentaux.

Mais le code frontal ne consiste pas à manipuler des données. Il s'agit d'événements utilisateur et de rendre les bonnes vues au bon moment. Les frontaux concernent les utilisateurs.

Voici ce que nous voulons pouvoir faire :

  • Tester les événements utilisateur de React
  • Testez la réponse à ces événements
  • Assurez-vous que les bonnes choses s'affichent au bon moment
  • Exécutez des tests dans de nombreux navigateurs
  • Re-exécuter des tests sur les modifications de fichiers
  • Travailler avec des systèmes d'intégration continue comme Travis

Au cours des dix années où j'ai fait cela, je n'avais pas trouvé de moyen décent de tester l'interaction de l'utilisateur et de visualiser le rendu jusqu'à ce que je commence à fouiller dans React.

Tests unitaires React : composants de l'interface utilisateur

React est le moyen le plus simple d'atteindre ces objectifs. En partie, à cause de la façon dont cela nous oblige à concevoir des applications en utilisant des modèles testables, en partie, parce qu'il existe de fantastiques utilitaires de test React.

Si vous n'avez jamais utilisé React auparavant, vous devriez consulter mon livre React+d3.js . Il est orienté vers les visualisations, mais on me dit que c'est "une introduction légère et géniale" à React.

React nous oblige à tout construire en tant que « composants ». Vous pouvez considérer les composants React comme des widgets ou comme des morceaux de HTML avec une certaine logique. Ils suivent bon nombre des meilleurs principes de la programmation fonctionnelle, sauf qu'ils sont des objets.

Par exemple, étant donné le même ensemble de paramètres, un composant React rendra toujours la même sortie. Peu importe combien de fois il est rendu, peu importe qui le rend, peu importe où nous plaçons la sortie. Toujours les mêmes. Par conséquent, nous n'avons pas à effectuer d'échafaudages complexes pour tester les composants React. Ils ne se soucient que de leurs propriétés, aucun suivi des variables globales et des objets de configuration n'est requis.

Nous y parvenons en grande partie en évitant l'état. Vous appelleriez cela la transparence référentielle dans la programmation fonctionnelle. Je ne pense pas qu'il y ait un nom spécial pour cela dans React, mais les documents officiels recommandent d'éviter autant que possible l'utilisation de l'état.

Lorsqu'il s'agit de tester les interactions des utilisateurs, React nous couvre d'événements liés aux rappels de fonction. Il est facile de configurer des espions de test et de s'assurer qu'un événement de clic appelle la bonne fonction. Et parce que les composants React s'affichent eux-mêmes, nous pouvons simplement déclencher un événement de clic et vérifier le HTML pour les changements. Cela fonctionne car un composant React ne se soucie que de lui-même. Un clic ici ne change rien -bas. Nous n'aurons jamais à gérer un nid de gestionnaires d'événements, juste des appels de fonction bien définis.

Oh, et parce que React est magique, nous n'avons pas à nous soucier du DOM. React utilise le soi-disant DOM virtuel pour restituer les composants dans une variable JavaScript. Et une référence au DOM virtuel est tout ce dont nous avons besoin pour tester les composants React, vraiment.

C'est plutôt mignon.

TestUtils de React

React est livré avec une suite de TestUtils . Il y a même un testeur recommandé appelé Jest, mais je ne l'aime pas. Je vais vous expliquer pourquoi dans un instant. Tout d'abord, le TestUtils .

Nous les obtenons en faisant quelque chose comme require('react/addons').addons.TestUtils . C'est notre point d'entrée pour tester les interactions des utilisateurs et vérifier la sortie.

Les React TestUtils nous permettent de rendre un composant React en mettant son DOM dans une variable, au lieu de l'insérer dans une page. Par exemple, pour afficher un composant React, nous ferions quelque chose comme ceci :

 var component = TestUtils.renderIntoDocument( <MyComponent /> );

Ensuite, nous pouvons utiliser TestUtils pour vérifier si tous les enfants ont été rendus. Quelque chose comme ça:

 var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );

findRenderedDOMComponentWithTag fera ce que cela ressemble : parcourez les enfants, trouvez le composant que nous recherchons et renvoyez-le. La valeur renvoyée se comportera comme un composant React.

Nous pouvons ensuite utiliser getDOMNode() pour accéder à l'élément DOM brut et tester ses valeurs. Pour vérifier qu'une balise h1 dans le composant indique « A title » , nous écrirons ceci :

 expect(h1.getDOMNode().textContent) .toEqual("A title");

Mis ensemble, le test complet ressemblerait à ceci:

 it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });

La partie intéressante est que TestUtils nous permet également de déclencher des événements utilisateur. Pour un événement de clic, nous écrirons quelque chose comme ceci :

 var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);

Cela simule un clic et déclenche tous les écouteurs potentiels, qui doivent être des méthodes de composants qui modifient soit la sortie, soit l'état, soit les deux. Ces écouteurs peuvent appeler une fonction sur un composant parent si nécessaire.

Tous les cas sont simples à tester : l'état modifié est dans component.state , nous pouvons accéder à la sortie avec des fonctions DOM normales et des appels de fonction avec des espions.

Pourquoi ne pas plaisanter ?

La documentation officielle de React recommande d'utiliser https://facebook.github.io/jest/ comme exécuteur de test et cadre de test React. Jest est construit sur Jasmine et utilise la même syntaxe. En plus de tout ce que vous obtenez de Jasmine, Jest se moque également de tout sauf du composant que nous testons. C'est fantastique en théorie, mais je trouve cela ennuyeux. Tout ce que nous n'avons pas encore implémenté, ou qui provient d'une partie différente de la base de code, est simplement undefined . Bien que cela convienne dans de nombreux cas, cela peut entraîner l'échec discret de bogues.

J'ai eu du mal à tester un événement de clic, par exemple. Peu importe ce que j'essayais, il n'appelait tout simplement pas son auditeur. Ensuite, j'ai réalisé que la fonction avait été moquée par Jest et qu'elle ne m'avait jamais dit cela.

Mais la pire offense de Jest, de loin, était qu'il n'avait pas de mode montre pour tester automatiquement les nouveaux changements. Nous pourrions l'exécuter une fois, obtenir les résultats des tests, et c'est tout. (J'aime exécuter mes tests en arrière-plan pendant que je travaille. Sinon, j'oublie de les exécuter.) Aujourd'hui, ce n'est plus un problème.

Oh, et Jest ne prend pas en charge l'exécution de tests React dans plusieurs navigateurs. C'est moins un problème qu'auparavant, mais j'ai l'impression que c'est une fonctionnalité importante pour cette rare occasion où un heisenbug ne se produit que dans une version spécifique de Chrome…

Note de l'éditeur : depuis la rédaction de cet article, Jest s'est considérablement amélioré. Vous pouvez lire notre didacticiel le plus récent, React Unit Testing Using Enzyme and Jest, et décider par vous-même si le test Jest est à la hauteur de la tâche de nos jours.

React Testing : un exemple intégré

Quoi qu'il en soit, nous avons vu comment un bon test React frontal devrait fonctionner en théorie. Passons à l'action avec un court exemple.

Nous allons visualiser différentes manières de générer des nombres aléatoires à l'aide d'un composant de nuage de points réalisé avec React et d3.js. Le code et sa démo sont également sur Github.

Nous allons utiliser Karma comme lanceur de test, Mocha comme framework de test et Webpack comme chargeur de module.

La mise en place

Nos fichiers sources iront dans un répertoire <root>/src , et nous placerons les tests dans un répertoire <root>/src/__tests__ . L'idée est que nous pouvons mettre plusieurs répertoires dans src , un pour chaque composant majeur, et chacun avec ses propres fichiers de test. Regrouper le code source et les fichiers de test comme celui-ci facilite la réutilisation des composants React dans différents projets.

Avec la structure de répertoires en place, nous pouvons installer des dépendances comme celle-ci :

 $ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect

Si quelque chose ne s'installe pas, essayez de relancer cette partie de l'installation. Le NPM échoue parfois d'une manière qui disparaît lors d'une nouvelle exécution.

Notre fichier package.json devrait ressembler à ceci lorsque nous aurons terminé :

 // package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }

Après une certaine configuration, nous pourrons exécuter des tests avec npm test ou karma start .

Exécution d'un test

La configuration

Il n'y a pas grand chose dans la configuration. Nous devons nous assurer que Webpack sait comment trouver notre code et que Karma sait comment exécuter les tests.

Nous avons mis deux lignes de JavaScript dans un fichier ./tests.webpack.js pour aider Karma et Webpack à jouer ensemble :

 // tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);

Cela indique à Webpack de considérer tout ce qui a un suffixe -test comme faisant partie de la suite de tests.

Configurer Karma demande un peu plus de travail :

 // karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };

La plupart de ces lignes proviennent d'une configuration Karma par défaut. Nous avons utilisé des browsers pour indiquer que les tests doivent s'exécuter dans Chrome, des frameworks pour spécifier le framework de test que nous utilisons et singleRun pour que les tests ne s'exécutent qu'une seule fois par défaut. Vous pouvez continuer à faire fonctionner le karma en arrière-plan avec karma start --no-single-run .

Ces trois-là sont évidents. Les trucs Webpack sont plus intéressants.

Comme Webpack gère l'arborescence des dépendances de notre code, nous n'avons pas à spécifier tous nos fichiers dans le tableau files . Nous n'avons besoin que tests.webpack.js , qui nécessite ensuite tous les fichiers nécessaires.

Nous utilisons le paramètre webpack pour dire à Webpack quoi faire. Dans un environnement normal, cette partie irait dans un fichier webpack.config.js .

Nous disons également à Webpack d'utiliser le babel-loader pour nos JavaScripts. Cela nous donne toutes les nouvelles fonctionnalités sophistiquées d'ECMAScript2015 et de JSX de React.

Avec la configuration de webpackServer , nous disons à Webpack de ne pas imprimer d'informations de débogage. Cela ne ferait que gâcher notre sortie de test.

Un composant React et un test

Avec une suite de tests en cours d'exécution, le reste est simple. Nous devons créer un composant qui accepte un tableau de coordonnées aléatoires et crée un élément <svg> avec un ensemble de points.

En suivant les meilleures pratiques de test React, c'est-à-dire la pratique TDD standard, nous écrirons d'abord le test, puis le composant React réel. Commençons par un fichier de tests vanille dans src/__tests__/ :

 // ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });

Nous avons d'abord besoin de React, de ses TestUtils, d3.js, de la bibliothèque expect et du code que nous testons. Ensuite, nous créons une nouvelle suite de tests avec describe , et créons des données aléatoires.

Pour notre premier test, assurons-nous que ScatterPlot un titre. Notre test va à l'intérieur du bloc describe :

 // ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });

La plupart des tests suivront le même schéma :

  1. Rendre.
  2. Trouver un nœud spécifique.
  3. Vérifiez le contenu.

Comme nous l'avons vu précédemment, renderIntoDocument rend notre composant, findRenderedDOMComponentWithTag trouve la partie spécifique que nous testons et getDOMNode nous donne un accès DOM brut.

Au début, notre test échouera. Pour le faire passer, nous devons écrire le composant qui affiche une balise de titre :

 var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;

C'est ça. Le composant ScatterPlot rend un <div> avec une <h1> contenant le texte attendu, et notre test réussira. Oui, c'est plus long que du HTML, mais supportez-moi.

Dessinez le reste du hibou

Vous pouvez voir le reste de notre exemple sur GitHub, comme mentionné ci-dessus. Nous allons sauter la description étape par étape dans cet article, mais le processus général est le même que ci-dessus. Je veux vous montrer un test plus intéressant, cependant. Un test qui garantit que tous les points de données apparaissent sur le graphique :

 // ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });

Pareil qu'avant. Rendre, trouver des nœuds, vérifier le résultat. La partie intéressante ici est de dessiner ces nœuds DOM. Nous ajoutons un peu de magie ScatterPlot au composant ScatterPlot, comme ceci :

 // ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...

Nous utilisons componentWillMount pour configurer des échelles d3 vides pour les domaines X et Y , et componentWillReceiveProps pour nous assurer qu'elles sont mises à jour lorsque quelque chose change. Ensuite, update_d3 s'assure de définir le domain et la range pour les deux échelles.

Nous utiliserons les deux échelles pour traduire entre les valeurs aléatoires de notre ensemble de données et les positions sur l'image. La plupart des générateurs aléatoires renvoient des nombres dans la plage [0,1] , qui est trop petite pour être vue comme des pixels.

Ensuite, nous ajoutons les points à la méthode de rendu de notre composant :

 // ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }

Ce code passe par le tableau this.props.data et ajoute un élément <circle> pour chaque point de données. Simple.

Ne désespérez plus pour vos tests d'interface utilisateur, avec les tests de composants React.
Tweeter

Si vous voulez en savoir plus sur la combinaison de React et d3.js pour créer des composants de visualisation de données, c'est une autre bonne raison de consulter mon livre, React+d3.js .

Test automatisé des composants React : plus simple qu'il n'y paraît

C'est tout ce que nous devons savoir sur l'écriture de composants frontaux testables avec React. Pour voir plus de tests de code sur les composants React, consultez la base de code de l'exemple de test React sur Github, comme mentionné ci-dessus.

Nous avons appris que :

  1. React nous oblige à modulariser et à encapsuler.
  2. Cela rend les tests de l'interface utilisateur React faciles à automatiser.
  3. Les tests unitaires ne suffisent pas pour les frontaux.
  4. Karma est un excellent testeur.
  5. Jest a du potentiel, mais n'est pas encore là. (Ou peut-être qu'il l'est maintenant.)

Si vous avez aimé cet article, suivez-moi sur Twitter et laissez un commentaire ci-dessous. Merci d'avoir lu et bon test React !

En relation: Comment optimiser les composants pour améliorer les performances de React