Développement piloté par les tests React : des user stories à la production
Publié: 2022-03-11Dans cet article, nous allons développer une application React en utilisant le développement piloté par les tests (TDD), des user stories au développement. De plus, nous allons utiliser Jest et Enzyme pour le TDD. À la fin de ce guide, vous serez en mesure de :
- Créez des epics et des user stories en fonction des besoins.
- Créez des tests basés sur des user stories.
- Développer une application React à l'aide de TDD.
- Utilisez Enzyme et Jest pour tester une application React.
- Utilisez/réutilisez les variables CSS pour une conception réactive.
- Créez un composant React réutilisable qui s'affiche et fonctionne différemment en fonction des accessoires fournis.
- Tapez les accessoires de composant de vérification à l'aide de React PropTypes.
Cet article suppose que vous avez une connaissance de base de React. Si vous êtes complètement nouveau sur React, je vous recommande de suivre le didacticiel officiel et de jeter un œil au didacticiel React 2019 de Toptal : partie 1 et partie 2.
Présentation de notre application React pilotée par les tests
Nous allons créer une application de minuterie pomodoro de base composée de certains composants de l'interface utilisateur. Chaque composant aura un ensemble de tests distinct dans un fichier de test correspondant. Tout d'abord, nous pourrions créer des épopées et des histoires d'utilisateurs comme suit en fonction des exigences de notre projet.
ÉPIQUE | HISTOIRE DE L'UTILISATEUR | CRITÈRES D'ACCEPTATION |
En tant qu'utilisateur, j'ai besoin d'utiliser la minuterie pour pouvoir gérer mon temps. | En tant qu'utilisateur, je dois démarrer le chronomètre pour pouvoir compter mon temps. | Assurez-vous que l'utilisateur est capable de : * démarrer le chronomètre *voir le compte à rebours du compte à rebours Le compte à rebours ne doit pas être interrompu même si l'utilisateur clique plus d'une fois sur le bouton de démarrage. |
En tant qu'utilisateur, je dois arrêter le chronomètre pour ne pouvoir décompter mon temps qu'en cas de besoin. | Assurez-vous que l'utilisateur est capable de : * arrêter le chronomètre *voir le chronomètre arrêté Rien ne devrait se passer même si l'utilisateur clique plus d'une fois sur le bouton d'arrêt. | |
En tant qu'utilisateur, je dois réinitialiser la minuterie pour pouvoir compter mon temps depuis le début. | Assurez-vous que l'utilisateur est capable de : * réinitialiser la minuterie *voir la minuterie remise à la valeur par défaut |
Filaire
Configuration du projet
Tout d'abord, nous allons créer un projet React en utilisant Create React App comme suit :
$ npx create-react-app react-timer $ cd react-timer $ npm start
Vous verrez un nouvel onglet de navigateur ouvert à l'URL http://localhost:3000. Vous pouvez arrêter l'application React en cours d'exécution en utilisant Ctrl+C .
Maintenant, nous allons ajouter Jest et Enzyme et quelques dépendances comme suit :
$ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16
De plus, nous ajouterons ou mettrons à jour un fichier appelé setupTests.js dans le répertoire src :
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Étant donné que Create React App exécute le fichier setupTests.js avant chaque test, il exécutera et configurera correctement Enzyme.
Configuration CSS
Nous allons écrire des variables et une réinitialisation CSS de base car nous voulons que les variables CSS soient globalement disponibles dans l'application. Nous définirons les variables à partir de la portée :root. La syntaxe de définition des variables consiste à utiliser une notation de propriété personnalisée, chacune commençant par – suivi du nom de la variable.
Accédez au fichier index.css et ajoutez ce qui suit :
:root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }
Maintenant, nous devons importer le CSS dans notre application. Mettez à jour le fichier index.js comme suit :
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )
Test de rendu peu profond
Comme vous le savez peut-être déjà, le processus TDD ressemblerait à ceci :
- Ajouter un essai.
- Exécutez tous les tests et vous verrez que le test échoue.
- Écrivez le code pour réussir le test.
- Exécutez tous les tests.
- Refactoriser.
- Répéter.
Par conséquent, nous allons ajouter le premier test pour un test de rendu superficiel, puis écrire le code pour réussir le test. Ajoutez un nouveau fichier de spécification nommé App.spec.js au répertoire src/components/App comme suit :
import React from 'react'; import { shallow } from 'enzyme'; import App from './App'; describe('App', () => { it('should render a <div />', () => { const container = shallow(<App />); expect(container.find('div').length).toEqual(1); }); });
Ensuite, vous pouvez lancer le test :
$ npm test
Vous verrez que le test échoue.
Composant d'application
Nous allons maintenant créer le composant App pour réussir le test. Accédez à App.jsx dans le répertoire src/components/App et ajoutez le code comme suit :
import React from 'react'; const App = () => <div className=”app-container” />; export default App;
Maintenant, relancez le test.
$ npm test
Le premier test devrait maintenant réussir.
Ajout de CSS d'application
Nous allons créer un fichier App.css dans le répertoire src/components/App pour ajouter du style au composant App comme suit :
.app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }
Nous sommes maintenant prêts à importer le CSS dans le fichier App.jsx :
import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;
Ensuite, nous devons mettre à jour le fichier index.js pour importer le composant App comme suit :
import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./components/App/App" import * as serviceWorker from "./serviceWorker" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister()
Ajout du composant de minuterie
Enfin, l'application contiendra le composant Timer, nous allons donc mettre à jour le fichier App.spec.js pour vérifier la présence du composant Timer dans notre application. De plus, nous allons déclarer la variable de conteneur en dehors du premier cas de test, car le test de rendu peu profond doit être effectué avant chaque cas de test.
import React from "react" import { shallow } from "enzyme" import App from "./App" import Timer from "../Timer/Timer" describe("App", () => { let container beforeEach(() => (container = shallow(<App />))) it("should render a <div />", () => { expect(container.find("div").length).toEqual(1) }) it("should render the Timer Component", () => { expect(container.containsMatchingElement(<Timer />)).toEqual(true) }) })
Si vous exécutez npm test
à ce stade, le test échouera car le composant Timer n'existe pas encore.
Écriture du test de rendu superficiel de la minuterie
Maintenant, nous allons créer un fichier nommé Timer.spec.js dans un nouveau répertoire nommé Timer sous le répertoire src/components .
De plus, nous ajouterons le test de rendu superficiel dans le fichier Timer.spec.js :
import React from "react" import { shallow } from "enzyme" import Timer from "./Timer" describe("Timer", () => { let container beforeEach(() => (container = shallow(<Timer />))) it("should render a <div />", () => { expect(container.find("div").length).toBeGreaterThanOrEqual(1) }) })
Le test échouera, comme prévu.
Création du composant de minuterie
Ensuite, créons un nouveau fichier appelé Timer.jsx et définissons les mêmes variables et méthodes basées sur les user stories :
import React, { Component } from 'react'; class Timer extends Component { constructor(props) { super(props); this.state = { minutes: 25, seconds: 0, isOn: false }; } startTimer() { console.log('Starting timer.'); } stopTimer() { console.log('Stopping timer.'); } resetTimer() { console.log('Resetting timer.'); } render = () => { return <div className="timer-container" />; }; } export default Timer;
Cela devrait réussir le test et devrait rendre un <div />
dans le fichier Timer.spec.js , mais le test ne devrait pas rendre le composant Timer car nous n'avons pas encore ajouté le composant Timer dans le composant d'application.
Nous allons ajouter le composant Timer dans le fichier App.jsx comme ceci :
import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;
Tous les tests devraient réussir maintenant.
Ajout de la minuterie CSS
Nous allons ajouter des variables CSS liées au minuteur et ajouter des requêtes multimédias pour les petits appareils.
Mettez à jour le fichier index.css comme suit :
:root { --timer-background-color: #FFFFFF; --timer-border: 1px solid #000000; --timer-height: 70%; --timer-width: 70%; } body, div, p { margin: 0; padding: 0; } @media screen and (max-width: 1024px) { :root { --timer-height: 100%; --timer-width: 100%; } }
Aussi, nous allons créer le fichier Timer.css sous le répertoire components/Timer :
.timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }
Nous devons mettre à jour Timer.jsx pour importer le fichier Timer.css .

import React, { Component } from "react" import "./Timer.css"
Si vous exécutez l'application React maintenant, vous verrez un écran simple avec la bordure sur votre navigateur.
Écrire le test de rendu superficiel de TimerButton
Nous avons besoin de trois boutons : Start, Stop et Reset , nous allons donc créer le TimerButton Component .
Tout d'abord, nous devons mettre à jour le fichier Timer.spec.js pour vérifier l'existence du composant TimerButton dans le composant Timer :
it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })
Maintenant, ajoutons le fichier TimerButton.spec.js dans un nouveau répertoire appelé TimerButton sous le répertoire src/components et ajoutons le test au fichier comme ceci :
import React from "react" import { shallow } from "enzyme" import TimerButton from "./TimerButton" describe("TimerButton", () => { let container beforeEach(() => { container = shallow( <TimerButton buttonAction={jest.fn()} buttonValue={""} /> ) }) it("should render a <div />", () => { expect(container.find("div").length).toBeGreaterThanOrEqual(1) }) })
Maintenant, si vous exécutez le test, vous verrez que le test échoue.
Créons le fichier TimerButton.jsx pour le composant TimerButton :
import React from 'react'; import PropTypes from 'prop-types'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" /> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;
Si vous exécutez npm test
à ce stade, le test devrait restituer les instances du composant TimerButton mais échouera car nous n'avons pas encore ajouté les composants TimerButton au composant Timer.
Importons le composant TimerButton et ajoutons trois composants TimerButton dans la méthode de rendu dans Timer.jsx :
render = () => { return ( <div className="timer-container"> <div className="time-display"></div> <div className="timer-button-container"> <TimerButton buttonAction={this.startTimer} buttonValue={'Start'} /> <TimerButton buttonAction={this.stopTimer} buttonValue={'Stop'} /> <TimerButton buttonAction={this.resetTimer} buttonValue={'Reset'} /> </div> </div> ); };
TimerButton CSS
Il est maintenant temps d'ajouter des variables CSS pour le composant TimerButton. Ajoutons des variables dans la portée :root au fichier index.css :
:root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }
Créons également un fichier appelé TimerButton.css dans le répertoire TimerButton sous le répertoire src/components :
.button-container { flex: 1 1 auto; text-align: center; margin: 0px 20px; border: var(--button-border); font-size: var(--button-text-size); } .button-container:hover { cursor: pointer; }
Mettons à jour le TimerButton.jsx en conséquence pour importer le fichier TimerButton.css et afficher la valeur du bouton :
import React from 'react'; import PropTypes from 'prop-types'; import './TimerButton.css'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container"> <p className="button-value">{buttonValue}</p> </div> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;
De plus, nous devons mettre à jour le Timer.css pour aligner les trois boutons horizontalement, alors mettons également à jour le fichier Timer.css :
import React from 'react'; import PropTypes from 'prop-types'; import './TimerButton.css'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container"> <p className="button-value">{buttonValue}</p> </div> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;
Si vous exécutez l'application React maintenant, vous verrez un écran comme suit :

Refactoriser la minuterie
Nous allons refactoriser le Timer car nous voulons implémenter des fonctions telles que startTimer, stopTimer, restartTimer et resetTimer . Commençons par mettre à jour le fichier Timer.spec.js :
describe('mounted Timer', () => { let container; beforeEach(() => (container = mount(<Timer />))); it('invokes startTimer when the start button is clicked', () => { const spy = jest.spyOn(container.instance(), 'startTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.start-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); it('invokes stopTimer when the stop button is clicked', () => { const spy = jest.spyOn(container.instance(), 'stopTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.stop-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); it('invokes resetTimer when the reset button is clicked', () => { const spy = jest.spyOn(container.instance(), 'resetTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.reset-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); });
Si vous exécutez le test, vous verrez les tests ajoutés échouer car nous n'avons pas encore mis à jour le composant TimerButton . Mettons à jour le composant TimerButton pour ajouter l'événement click :
const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );
Maintenant, les tests devraient réussir.
Ensuite, nous allons ajouter d'autres tests pour vérifier l'état lorsque chaque fonction est invoquée dans le cas de test Timer monté :
it('should change isOn state true when the start button is clicked', () => { container.instance().forceUpdate(); container.find('.start-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(true); }); it('should change isOn state false when the stop button is clicked', () => { container.instance().forceUpdate(); container.find('.stop-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(false); }); it('should change isOn state false when the reset button is clicked', () => { container.instance().forceUpdate(); container.find('.stop-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(false); expect(container.instance().state.minutes).toEqual(25); expect(container.instance().state.seconds).toEqual(0); });
Si vous exécutez les tests, vous les verrez échouer car nous n'avons pas encore implémenté chaque méthode. Implémentons donc chaque fonction pour réussir les tests :
startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }
Vous verrez les tests passer si vous les lancez. Maintenant, implémentons les fonctions restantes dans Timer.jsx :
import React, { Component } from 'react'; import './Timer.css'; import TimerButton from '../TimerButton/TimerButton'; class Timer extends Component { constructor(props) { super(props); this.state = { minutes: 25, seconds: 0, isOn: false, }; this.startTimer = this.startTimer.bind(this); this.stopTimer = this.stopTimer.bind(this); this.resetTimer = this.resetTimer.bind(this); } startTimer() { if (this.state.isOn === true) { return; } this.myInterval = setInterval(() => { const { seconds, minutes } = this.state; if (seconds > 0) { this.setState(({ seconds }) => ({ seconds: seconds - 1, })); } if (seconds === 0) { if (minutes === 0) { clearInterval(this.myInterval); } else { this.setState(({ minutes }) => ({ minutes: minutes - 1, seconds: 59, })); } } }, 1000); this.setState({ isOn: true }); } stopTimer() { clearInterval(this.myInterval); this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); } render = () => { const { minutes, seconds } = this.state; return ( <div className="timer-container"> <div className="time-display"> {minutes}:{seconds < 10 ? `0${seconds}` : seconds} </div> <div className="timer-button-container"> <TimerButton className="start-timer" buttonAction={this.startTimer} buttonValue={'Start'} /> <TimerButton className="stop-timer" buttonAction={this.stopTimer} buttonValue={'Stop'} /> <TimerButton className="reset-timer" buttonAction={this.resetTimer} buttonValue={'Reset'} /> </div> </div> ); }; } export default Timer;
Vous verrez toutes les fonctions fonctionner sur la base des user stories que nous avons préparées précédemment.
C'est ainsi que nous avons développé une application React de base en utilisant TDD. Si les user stories et les critères d'acceptation sont plus détaillés, les cas de test peuvent être écrits plus précisément, contribuant ainsi encore plus.
Emballer
Lors du développement d'une application à l'aide de TDD, il est très important non seulement de décomposer le projet en epics ou user stories, mais aussi de bien se préparer aux critères d'acceptation. Dans cet article, je voulais vous montrer comment décomposer le projet et utiliser les critères d'acceptation préparés pour le développement de React TDD.
Même s'il existe de nombreuses ressources liées à React TDD, j'espère que cet article vous a aidé à en apprendre un peu plus sur le développement de TDD avec React à l'aide de user stories. Si vous choisissez d'émuler cette approche, veuillez vous référer au code source complet ici.