React Dezvoltare bazată pe teste: de la poveștile utilizatorilor la producție
Publicat: 2022-03-11În această postare, vom dezvolta o aplicație React folosind dezvoltarea bazată pe teste (TDD) de la poveștile utilizatorilor până la dezvoltare. De asemenea, vom folosi Jest și Enzyme pentru TDD. După finalizarea acestui ghid, veți putea:
- Creați epopee și povești de utilizator pe baza cerințelor.
- Creați teste bazate pe poveștile utilizatorilor.
- Dezvoltați o aplicație React folosind TDD.
- Utilizați Enzyme și Jest pentru a testa o aplicație React.
- Utilizați/reutilizați variabile CSS pentru design receptiv.
- Creați o componentă React reutilizabilă care redă și funcționează diferit pe baza elementelor de recuzită furnizate.
- Tip verificați elementele de recuzită pentru componente folosind React PropTypes.
Acest articol presupune că aveți cunoștințe de bază despre React. Dacă sunteți complet nou în React, vă recomand să finalizați tutorialul oficial și să aruncați o privire la Tutorialul React 2019 de la Toptal: partea 1 și partea 2.
Prezentare generală a aplicației noastre React bazate pe teste
Vom construi o aplicație de bază de cronometru pomodoro, constând din câteva componente ale interfeței de utilizare. Fiecare componentă va avea un set separat de teste într-un fișier de test corespunzător. În primul rând, am putea crea epopee și povești de utilizator după cum urmează, pe baza cerințelor proiectului nostru.
EPIC | POVESTEA UTILIZATORULUI | CRITERIUL DE ACCEPTARE |
Ca utilizator, trebuie să folosesc cronometrul pentru a-mi putea gestiona timpul. | În calitate de utilizator, trebuie să pornesc cronometrul, astfel încât să îmi pot număra timpul. | Asigurați-vă că utilizatorul este capabil să: *porniți cronometrul * vezi ca cronometrul începe numărătoarea inversă Numărătoarea inversă a timpului nu trebuie întreruptă chiar dacă utilizatorul face clic pe butonul de pornire de mai multe ori. |
În calitate de utilizator, trebuie să opresc cronometrul, astfel încât să îmi pot număra timpul numai când este nevoie. | Asigurați-vă că utilizatorul este capabil să: *opriți cronometrul * vezi cronometrul oprit Nimic nu ar trebui să se întâmple chiar dacă utilizatorul face clic pe butonul de oprire de mai multe ori. | |
Ca utilizator, trebuie să resetez cronometrul, astfel încât să îmi pot număra timpul de la început. | Asigurați-vă că utilizatorul este capabil să: *resetați cronometrul *vezi resetarea temporizatorului la valoarea implicită |
Cadru de sarma
Configurarea proiectului
În primul rând, vom crea un proiect React utilizând aplicația Create React , după cum urmează:
$ npx create-react-app react-timer $ cd react-timer $ npm start
Veți vedea o nouă filă de browser deschisă la adresa URL http://localhost:3000. Puteți opri rularea aplicației React folosind Ctrl+C .
Acum, vom adăuga Jest și Enzyme și câteva dependențe, după cum urmează:
$ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16
De asemenea, vom adăuga sau actualiza un fișier numit setupTests.js în directorul src :
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Deoarece Create React App rulează fișierul setupTests.js înainte de fiecare test, acesta va executa și configura corect Enzyme.
Configurarea CSS
Vom scrie variabile și o resetare CSS de bază, deoarece dorim ca variabilele CSS să fie disponibile la nivel global în aplicație. Vom defini variabilele din domeniul :root. Sintaxa pentru definirea variabilelor este de a utiliza notația personalizată a proprietăților, fiecare începând cu – urmată de numele variabilei.
Navigați la fișierul index.css și adăugați următoarele:
:root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }
Acum, trebuie să importam CSS-ul în aplicația noastră. Actualizați fișierul index.js după cum urmează:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )
Test de randare superficială
După cum probabil știți deja, procesul TDD ar arăta astfel:
- Adăugați un test.
- Rulați toate testele și veți vedea că testul eșuează.
- Scrieți codul pentru a trece testul.
- Rulați toate testele.
- Refactorizare.
- Repeta.
Prin urmare, vom adăuga primul test pentru un test de randare superficială și apoi vom scrie codul pentru a trece testul. Adăugați un nou fișier cu specificații numit App.spec.js în directorul src/components/App , după cum urmează:
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); }); });
Apoi, puteți rula testul:
$ npm test
Veți vedea că testul eșuează.
Componenta aplicației
Acum, vom continua să creăm componenta App pentru a trece testul. Navigați la App.jsx în directorul src/components/App și adăugați codul după cum urmează:
import React from 'react'; const App = () => <div className=”app-container” />; export default App;
Acum, rulați din nou testul.
$ npm test
Primul test ar trebui să treacă acum.
Adăugarea aplicației CSS
Vom crea un fișier App.css în directorul src/components/App pentru a adăuga un stil la componenta App, după cum urmează:
.app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }
Acum, suntem gata să importam CSS în fișierul App.jsx :
import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;
Apoi, trebuie să actualizăm fișierul index.js pentru a importa componenta aplicației după cum urmează:
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()
Adăugarea componentei Timer
În cele din urmă, aplicația va conține componenta Timer, prin urmare vom actualiza fișierul App.spec.js pentru a verifica prezența componentei Timer în aplicația noastră. De asemenea, vom declara variabila container în afara primului caz de testare, deoarece testul de randare superficială trebuie făcut înainte de fiecare caz de testare.
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) }) })
Dacă rulați npm test
în această etapă, testul va eșua, deoarece componenta Timer nu există încă.
Scrierea testului de randare superficială a temporizatorului
Acum, vom crea un fișier numit Timer.spec.js într-un nou director numit Timer sub directorul src/components .
De asemenea, vom adăuga testul de randare superficială în fișierul 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) }) })
Testul va eșua, așa cum era de așteptat.
Crearea componentei Timer
Apoi, să creăm un nou fișier numit Timer.jsx și să definim aceleași variabile și metode pe baza poveștilor utilizatorilor:
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;
Acest lucru ar trebui să treacă testul și ar trebui să redeze un <div />
în fișierul Timer.spec.js , dar testul nu ar trebui să redea Componenta Timer, deoarece nu am adăugat încă componenta Timer în componenta aplicației.
Vom adăuga componenta Timer în fișierul App.jsx astfel:
import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;
Toate testele ar trebui să treacă acum.
Adăugarea Timer CSS
Vom adăuga variabile CSS legate de Timer și vom adăuga interogări media pentru dispozitive mai mici.
Actualizați fișierul index.css după cum urmează:
: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%; } }
De asemenea, vom crea fișierul Timer.css sub directorul componente/Timer :
.timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }
Trebuie să actualizăm Timer.jsx pentru a importa fișierul Timer.css .

import React, { Component } from "react" import "./Timer.css"
Dacă rulați aplicația React acum, veți vedea un ecran simplu cu chenar pe browser.
Scrieți testul de randare superficială TimerButton
Avem nevoie de trei butoane: Start, Stop și Reset , prin urmare vom crea Componenta TimerButton .
Mai întâi, trebuie să actualizăm fișierul Timer.spec.js pentru a verifica existența componentei TimerButton în componenta Timer :
it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })
Acum, să adăugăm fișierul TimerButton.spec.js într-un nou director numit TimerButton sub directorul src/components și să adăugăm testul la fișier astfel:
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) }) })
Acum, dacă rulați testul, veți vedea că testul eșuează.
Să creăm fișierul TimerButton.jsx pentru componenta 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;
Dacă rulați npm test
în această etapă, testul ar trebui să redă instanțe ale componentei TimerButton, dar va eșua, deoarece nu am adăugat încă componentele TimerButton la componenta Timer.
Să importăm componenta TimerButton și să adăugăm trei componente TimerButton în metoda de randare în 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
Acum, este timpul să adăugați variabile CSS pentru componenta TimerButton. Să adăugăm variabile în domeniul :root în fișierul index.css :
:root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }
De asemenea, să creăm un fișier numit TimerButton.css în directorul TimerButton din directorul 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; }
Să actualizăm TimerButton.jsx în consecință pentru a importa fișierul TimerButton.css și pentru a afișa valoarea butonului:
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 asemenea, trebuie să actualizăm Timer.css pentru a alinia cele trei butoane pe orizontală, așa că haideți să actualizăm și fișierul 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;
Dacă rulați aplicația React acum, veți vedea un ecran după cum urmează:

Refactorizarea temporizatorului
Vom refactoriza cronometrul, deoarece dorim să implementăm funcții precum startTimer, stopTimer, restartTimer și resetTimer . Să actualizăm mai întâi fișierul 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); }); });
Dacă executați testul, veți vedea că testele adăugate eșuează, deoarece nu am actualizat încă componenta TimerButton . Să actualizăm componenta TimerButton pentru a adăuga evenimentul clic:
const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );
Acum, testele ar trebui să treacă.
În continuare, vom adăuga mai multe teste pentru a verifica starea când fiecare funcție este invocată în cazul de testare Timer montat :
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); });
Dacă rulați testele, veți vedea că ele eșuează, deoarece nu am implementat încă fiecare metodă. Deci, să implementăm fiecare funcție pentru a trece testele:
startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }
Veți vedea că testele trec dacă le rulați. Acum, să implementăm funcțiile rămase în 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;
Veți vedea că toate funcțiile funcționează pe baza poveștilor utilizatorilor pe care le-am pregătit mai devreme.
Deci, așa am dezvoltat o aplicație React de bază folosind TDD. Dacă poveștile utilizatorilor și criteriile de acceptare sunt mai detaliate, cazurile de testare pot fi scrise mai precis, contribuind astfel și mai mult.
Încheierea
Când dezvoltați o aplicație folosind TDD, este foarte important nu numai să descompuneți proiectul în epopee sau povești de utilizator, ci și să vă pregătiți bine pentru criteriile de acceptare. În acest articol, am vrut să vă arăt cum să defalcați proiectul și să utilizați criteriile de acceptare pregătite pentru dezvoltarea React TDD.
Chiar dacă există multe resurse legate de React TDD, sper că acest articol te-a ajutat să înveți puțin despre dezvoltarea TDD cu React folosind poveștile utilizatorilor. Dacă alegeți să emulați această abordare, vă rugăm să consultați codul sursă complet aici.