Reagire allo sviluppo basato sui test: dalle storie degli utenti alla produzione
Pubblicato: 2022-03-11In questo post, svilupperemo un'app React utilizzando lo sviluppo basato su test (TDD) dalle storie degli utenti allo sviluppo. Inoltre, useremo Jest ed Enzyme per il TDD. Al completamento di questa guida, sarai in grado di:
- Crea epiche e storie utente in base ai requisiti.
- Crea test basati sulle storie degli utenti.
- Sviluppa un'app React utilizzando TDD.
- Usa Enzima e Jest per testare un'app React.
- Usa/riutilizza le variabili CSS per il design reattivo.
- Crea un componente React riutilizzabile che esegue il rendering e funziona in modo diverso in base agli oggetti di scena forniti.
- Tipo di oggetti di scena dei componenti di controllo utilizzando React PropTypes.
Questo articolo presuppone che tu abbia una conoscenza di base di React. Se non conosci React, ti consiglio di completare il tutorial ufficiale e di dare un'occhiata al Tutorial React 2019 di Toptal: Parte 1 e Parte 2.
Panoramica della nostra app React basata su test
Creeremo un'app timer pomodoro di base composta da alcuni componenti dell'interfaccia utente. Ciascun componente avrà un insieme separato di test in un file di test corrispondente. Prima di tutto, potremmo creare epiche e storie degli utenti come segue in base ai requisiti del nostro progetto.
EPICO | STORIA DELL'UTENTE | CRITERI DI ACCETTAZIONE |
Come utente, devo usare il timer in modo da poter gestire il mio tempo. | Come utente, devo avviare il timer in modo da poter contare il mio tempo. | Assicurarsi che l'utente sia in grado di: *avviare il timer *vedi il conto alla rovescia del timer Il conto alla rovescia del tempo non deve essere interrotto anche se l'utente fa clic sul pulsante di avvio più di una volta. |
Come utente, devo fermare il timer in modo da poter contare il mio tempo solo quando necessario. | Assicurarsi che l'utente sia in grado di: *fermare il timer *vedi il timer fermo Non dovrebbe succedere nulla anche se l'utente fa clic sul pulsante di arresto più di una volta. | |
Come utente, devo reimpostare il timer in modo da poter contare il mio tempo dall'inizio. | Assicurarsi che l'utente sia in grado di: *reimposta il timer *vedi il timer ripristinato ai valori predefiniti |
Wireframe
Configurazione del progetto
Innanzitutto, creeremo un progetto React utilizzando l' app Create React come segue:
$ npx create-react-app react-timer $ cd react-timer $ npm start
Vedrai una nuova scheda del browser aperta all'URL http://localhost:3000. Puoi interrompere l'esecuzione dell'app React usando Ctrl+C .
Ora aggiungeremo Jest ed Enzyme e alcune dipendenze come segue:
$ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16
Inoltre, aggiungeremo o aggiorneremo un file chiamato setupTests.js nella directory src :
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Poiché l' app Create React esegue il file setupTests.js prima di ogni test, eseguirà e configurerà correttamente Enzyme.
Configurazione CSS
Scriveremo variabili e un ripristino CSS di base perché vogliamo che le variabili CSS siano disponibili globalmente nell'applicazione. Definiremo le variabili dall'ambito :root. La sintassi per la definizione delle variabili consiste nell'utilizzare la notazione delle proprietà personalizzate, ciascuna che inizia con – seguita dal nome della variabile.
Passare al file index.css e aggiungere quanto segue:
:root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }
Ora, dobbiamo importare il CSS nella nostra applicazione. Aggiorna il file index.js come segue:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )
Test di rendering superficiale
Come forse già saprai, il processo TDD sarebbe simile a questo:
- Aggiungi un test.
- Esegui tutti i test e vedrai che il test fallisce.
- Scrivi il codice per superare il test.
- Esegui tutti i test.
- Rifattore.
- Ripetere.
Quindi, aggiungeremo il primo test per un test di rendering superficiale e quindi scriveremo il codice per superare il test. Aggiungi un nuovo file delle specifiche denominato App.spec.js alla directory src/components/App come segue:
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); }); });
Quindi, puoi eseguire il test:
$ npm test
Vedrai che il test fallisce.
Componente dell'app
Ora procederemo alla creazione del componente App per superare il test. Passare a App.jsx nella directory src/components/App e aggiungere il codice come segue:
import React from 'react'; const App = () => <div className=”app-container” />; export default App;
Ora, esegui di nuovo il test.
$ npm test
Il primo test dovrebbe ora essere superato.
Aggiunta dell'app CSS
Creeremo un file App.css nella directory src/components/App per aggiungere uno stile al componente App come segue:
.app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }
Ora siamo pronti per importare il CSS nel file App.jsx :
import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;
Successivamente, dobbiamo aggiornare il file index.js per importare il componente App come segue:
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()
Aggiunta del componente timer
Infine, l'app conterrà il componente Timer, quindi aggiorneremo il file App.spec.js per verificare la presenza del componente Timer nella nostra app. Inoltre, dichiareremo la variabile contenitore al di fuori del primo test case poiché il test di rendering superficiale deve essere eseguito prima di ogni test case.
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) }) })
Se esegui npm test
in questa fase, il test avrà esito negativo poiché il componente Timer non esiste ancora.
Scrivere il test di rendering superficiale del timer
Ora creeremo un file chiamato Timer.spec.js in una nuova directory denominata Timer nella directory src/components .
Inoltre, aggiungeremo il test di rendering superficiale nel file 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) }) })
Il test fallirà, come previsto.
Creazione del componente timer
Quindi, creiamo un nuovo file chiamato Timer.jsx e definiamo le stesse variabili e metodi in base alle storie degli utenti:
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;
Questo dovrebbe superare il test e dovrebbe eseguire il rendering di un <div />
nel file Timer.spec.js , ma il test non dovrebbe eseguire il rendering del componente Timer poiché non abbiamo ancora aggiunto il componente Timer nel componente dell'app.
Aggiungeremo il componente Timer nel file App.jsx in questo modo:
import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;
Tutti i test dovrebbero passare ora.
Aggiunta di CSS timer
Aggiungeremo variabili CSS relative al timer e aggiungeremo query multimediali per dispositivi più piccoli.
Aggiorna il file index.css come segue:
: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%; } }
Inoltre, creeremo il file Timer.css nella directory Components/Timer :
.timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }
Dobbiamo aggiornare Timer.jsx per importare il file Timer.css .

import React, { Component } from "react" import "./Timer.css"
Se esegui ora l'app React, vedrai una semplice schermata con il bordo sul tuo browser.
Scrivi il test di rendering superficiale di TimerButton
Abbiamo bisogno di tre pulsanti: Start, Stop e Reset , quindi creeremo il componente TimerButton .
Innanzitutto, dobbiamo aggiornare il file Timer.spec.js per verificare l'esistenza del componente TimerButton nel componente Timer :
it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })
Ora aggiungiamo il file TimerButton.spec.js in una nuova directory chiamata TimerButton nella directory src/components e aggiungiamo il test al file in questo modo:
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) }) })
Ora, se esegui il test, vedrai che il test fallisce.
Creiamo il file TimerButton.jsx per il componente 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;
Se esegui npm test
in questa fase, il test dovrebbe eseguire il rendering delle istanze del componente TimerButton ma fallirà poiché non abbiamo ancora aggiunto i componenti TimerButton al componente Timer.
Importiamo il componente TimerButton e aggiungiamo tre componenti TimerButton nel metodo di rendering in 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> ); };
Pulsante Timer CSS
Ora è il momento di aggiungere variabili CSS per il componente TimerButton. Aggiungiamo le variabili nell'ambito :root al file index.css :
:root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }
Inoltre, creiamo un file chiamato TimerButton.css nella directory TimerButton nella directory 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; }
Aggiorniamo il TimerButton.jsx di conseguenza per importare il file TimerButton.css e per visualizzare il valore del pulsante:
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;
Inoltre, dobbiamo aggiornare Timer.css per allineare i tre pulsanti orizzontalmente, quindi aggiorniamo anche il file 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;
Se esegui ora l'app React, vedrai una schermata come segue:

Refactoring del timer
Faremo il refactoring del Timer poiché vogliamo implementare funzioni come startTimer, stopTimer, restartTimer e resetTimer . Aggiorniamo prima il file 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); }); });
Se esegui il test, vedrai che i test aggiunti falliscono poiché non abbiamo ancora aggiornato il componente TimerButton . Aggiorniamo il componente TimerButton per aggiungere l'evento click:
const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );
Ora, le prove dovrebbero passare.
Successivamente, aggiungeremo più test per verificare lo stato quando ogni funzione viene invocata nel test case Timer montato :
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); });
Se esegui i test, li vedrai fallire poiché non abbiamo ancora implementato ciascun metodo. Quindi implementiamo ogni funzione per superare i test:
startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }
Vedrai passare i test se li esegui. Ora, implementiamo le restanti funzioni in 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;
Vedrai che tutte le funzioni funzionano in base alle storie degli utenti che abbiamo preparato in precedenza.
Quindi, è così che abbiamo sviluppato un'app React di base utilizzando TDD. Se le storie degli utenti e i criteri di accettazione sono più dettagliati, i test case possono essere scritti in modo più preciso, contribuendo così ancora di più.
Avvolgendo
Quando si sviluppa un'applicazione utilizzando TDD, è molto importante non solo suddividere il progetto in epiche o storie degli utenti, ma anche prepararsi bene per i criteri di accettazione. In questo articolo, volevo mostrarti come scomporre il progetto e utilizzare i criteri di accettazione preparati per lo sviluppo di React TDD.
Anche se ci sono molte risorse relative a React TDD là fuori, spero che questo articolo ti abbia aiutato a imparare un po' sullo sviluppo di TDD con React usando le storie degli utenti. Se si sceglie di emulare questo approccio, fare riferimento al codice sorgente completo qui.