Testgetriebene Entwicklung reagieren: Von User Stories bis zur Produktion

Veröffentlicht: 2022-03-11

In diesem Beitrag entwickeln wir eine React-App mit testgetriebener Entwicklung (TDD) von User Stories bis hin zur Entwicklung. Außerdem werden wir Jest und Enzyme für die TDD verwenden. Nach Abschluss dieses Leitfadens können Sie:

  • Erstellen Sie Epics und User Stories basierend auf den Anforderungen.
  • Erstellen Sie Tests basierend auf User Stories.
  • Entwickeln Sie eine React-App mit TDD.
  • Verwenden Sie Enzyme und Jest, um eine React-App zu testen.
  • CSS-Variablen für responsives Design verwenden/wiederverwenden.
  • Erstellen Sie eine wiederverwendbare React-Komponente, die basierend auf den bereitgestellten Requisiten unterschiedlich gerendert und funktioniert.
  • Geben Sie Komponenten-Requisiten mit React PropTypes ein.

Dieser Artikel geht davon aus, dass Sie über Grundkenntnisse von React verfügen. Wenn Sie ganz neu bei React sind, empfehle ich Ihnen, das offizielle Tutorial zu absolvieren und einen Blick auf Toptals 2019 React Tutorial: Teil 1 und Teil 2 zu werfen.

Überblick über unsere testgetriebene React-App

Wir werden eine einfache Pomodoro-Timer-App erstellen, die aus einigen UI-Komponenten besteht. Jede Komponente verfügt über einen separaten Satz von Tests in einer entsprechenden Testdatei. Zunächst einmal konnten wir basierend auf unseren Projektanforderungen Epics und User Stories wie folgt erstellen.

EPOS BENUTZER GESCHICHTE AKZEPTANZKRITERIUM
Als Benutzer muss ich den Timer verwenden, damit ich meine Zeit verwalten kann. Als Benutzer muss ich den Timer starten, damit ich meine Zeit herunterzählen kann. Stellen Sie sicher, dass der Benutzer in der Lage ist:

* Starte den Timer
*Sehen Sie, wie der Timer herunterzählt

Das Herunterzählen der Zeit sollte auch dann nicht unterbrochen werden, wenn der Benutzer mehr als einmal auf den Start-Button klickt.
Als Benutzer muss ich den Timer anhalten, damit ich meine Zeit nur bei Bedarf herunterzählen kann. Stellen Sie sicher, dass der Benutzer in der Lage ist:

* Stoppen Sie den Timer
*siehe Stoppuhr

Es sollte nichts passieren, selbst wenn der Benutzer mehr als einmal auf die Stopp-Schaltfläche klickt.
Als Benutzer muss ich den Timer zurücksetzen, damit ich meine Zeit von Anfang an herunterzählen kann. Stellen Sie sicher, dass der Benutzer in der Lage ist:

* Timer zurücksetzen
* Sehen Sie, wie der Timer auf die Standardeinstellung zurückgesetzt wird

Drahtmodell

Drahtmodell

Projektaufbau

Zuerst werden wir ein React-Projekt mit Create React App wie folgt erstellen:

 $ npx create-react-app react-timer $ cd react-timer $ npm start

Unter der URL http://localhost:3000 wird ein neuer Browser-Tab geöffnet. Sie können die laufende React-App mit Strg+C stoppen.

Jetzt werden wir Jest und Enzyme und einige Abhängigkeiten wie folgt hinzufügen:

 $ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16

Außerdem werden wir eine Datei namens setupTests.js im src -Verzeichnis hinzufügen oder aktualisieren:

 import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });

Da Create React App die Datei setupTests.js vor jedem Test ausführt, wird sie Enzyme ausführen und richtig konfigurieren.

CSS konfigurieren

Wir werden Variablen und einen grundlegenden CSS-Reset schreiben, weil wir möchten, dass die CSS-Variablen global in der Anwendung verfügbar sind. Wir definieren die Variablen aus dem Geltungsbereich :root. Die Syntax zum Definieren von Variablen besteht darin, eine benutzerdefinierte Notation für Eigenschaften zu verwenden, die jeweils mit – gefolgt vom Variablennamen beginnt.

Navigieren Sie zur Datei index.css und fügen Sie Folgendes hinzu:

 :root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }

Jetzt müssen wir das CSS in unsere Anwendung importieren. Aktualisieren Sie die Datei index.js wie folgt:

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )

Flacher Rendertest

Wie Sie vielleicht bereits wissen, würde der TDD-Prozess wie folgt aussehen:

  1. Test hinzufügen.
  2. Führen Sie alle Tests durch, und Sie werden sehen, dass der Test fehlschlägt.
  3. Schreiben Sie den Code, um den Test zu bestehen.
  4. Führen Sie alle Tests durch.
  5. Umgestalten.
  6. Wiederholen.

Daher werden wir den ersten Test für einen flachen Rendertest hinzufügen und dann den Code schreiben, um den Test zu bestehen. Fügen Sie dem Verzeichnis src/components/App eine neue Spezifikationsdatei namens App.spec.js wie folgt hinzu:

 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); }); });

Dann können Sie den Test ausführen:

 $ npm test

Sie werden sehen, dass der Test fehlschlägt.

App-Komponente

Jetzt werden wir mit der Erstellung der App-Komponente fortfahren, um den Test zu bestehen. Navigieren Sie zu App.jsx im Verzeichnis src/components/App und fügen Sie den Code wie folgt hinzu:

 import React from 'react'; const App = () => <div className=”app-container” />; export default App;

Führen Sie den Test jetzt erneut aus.

 $ npm test

Der erste Test sollte nun bestanden werden.

Hinzufügen von App-CSS

Wir werden eine Datei App.css im Verzeichnis src/components/App erstellen, um der App-Komponente wie folgt etwas Stil hinzuzufügen:

 .app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }

Jetzt können wir das CSS in die App.jsx -Datei importieren:

 import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;

Als nächstes müssen wir die Datei index.js aktualisieren, um die App-Komponente wie folgt zu importieren:

 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()

Hinzufügen der Timer-Komponente

Schließlich enthält die App die Timer-Komponente, daher aktualisieren wir die App.spec.js -Datei, um zu prüfen, ob die Timer-Komponente in unserer App vorhanden ist. Außerdem werden wir die Containervariable außerhalb des ersten Testfalls deklarieren, da der flache Rendertest vor jedem Testfall durchgeführt werden muss.

 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) }) })

Wenn Sie zu diesem Zeitpunkt npm test ausführen, schlägt der Test fehl, da die Timer-Komponente noch nicht vorhanden ist.

Schreiben des Timer-Shallow-Rendering-Tests

Jetzt erstellen wir eine Datei namens Timer.spec.js in einem neuen Verzeichnis namens Timer im Verzeichnis src/components .

Außerdem werden wir den flachen Rendertest in der Datei Timer.spec.js hinzufügen :

 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) }) })

Der Test schlägt erwartungsgemäß fehl.

Erstellen der Timer-Komponente

Als Nächstes erstellen wir eine neue Datei mit dem Namen Timer.jsx und definieren dieselben Variablen und Methoden basierend auf den 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;

Dies sollte den Test bestehen und ein <div /> in der Datei Timer.spec.js rendern , aber der Test sollte die Timer-Komponente nicht rendern, da wir die Timer-Komponente noch nicht in der App-Komponente hinzugefügt haben.

Wir werden die Timer-Komponente in der App.jsx -Datei wie folgt hinzufügen:

 import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;

Alle Tests sollten jetzt bestanden werden.

Hinzufügen von Timer-CSS

Wir werden CSS-Variablen für den Timer hinzufügen und Medienabfragen für kleinere Geräte hinzufügen.

Aktualisieren Sie die Datei index.css wie folgt:

 :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%; } }

Außerdem erstellen wir die Datei Timer.css im Verzeichnis components/Timer :

 .timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }

Wir müssen Timer.jsx aktualisieren, um die Datei Timer.css zu importieren.

 import React, { Component } from "react" import "./Timer.css"

Wenn Sie die React-App jetzt ausführen, sehen Sie einen einfachen Bildschirm mit dem Rahmen in Ihrem Browser.

Schreiben Sie den TimerButton Shallow Rendering Test

Wir benötigen drei Schaltflächen: Start, Stop und Reset , daher erstellen wir die TimerButton-Komponente .

Zuerst müssen wir die Datei Timer.spec.js aktualisieren, um zu prüfen, ob die TimerButton- Komponente in der Timer -Komponente vorhanden ist:

 it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })

Fügen wir nun die Datei TimerButton.spec.js in einem neuen Verzeichnis namens TimerButton im Verzeichnis src/components hinzu und fügen wir den Test wie folgt zur Datei hinzu:

 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) }) })

Wenn Sie den Test jetzt ausführen, sehen Sie, dass der Test fehlschlägt.

Lassen Sie uns die Datei TimerButton.jsx für die TimerButton- Komponente erstellen:

 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;

Wenn Sie zu diesem Zeitpunkt npm test ausführen, sollte der Test Instanzen der TimerButton-Komponente rendern, schlägt jedoch fehl, da wir die TimerButton-Komponenten noch nicht zur Timer-Komponente hinzugefügt haben.

Importieren wir die TimerButton -Komponente und fügen drei TimerButton- Komponenten in der Rendermethode in Timer.jsx hinzu :

 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

Jetzt ist es an der Zeit, CSS-Variablen für die TimerButton-Komponente hinzuzufügen. Lassen Sie uns Variablen im Bereich :root zur Datei index.css hinzufügen :

 :root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }

Lassen Sie uns außerdem eine Datei namens TimerButton.css im Verzeichnis TimerButton unter dem Verzeichnis src/components erstellen:

 .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; }

Aktualisieren wir die Datei TimerButton.jsx entsprechend, um die Datei TimerButton.css zu importieren und den Schaltflächenwert anzuzeigen:

 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;

Außerdem müssen wir die Timer.css aktualisieren, um die drei Schaltflächen horizontal auszurichten, also aktualisieren wir auch die Timer.css -Datei:

 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;

Wenn Sie die React-App jetzt ausführen, sehen Sie einen Bildschirm wie folgt:

Timer

Refactoring des Timers

Wir werden den Timer umgestalten, da wir Funktionen wie startTimer, stopTimer, restartTimer und resetTimer implementieren möchten. Lassen Sie uns zuerst die Datei Timer.spec.js aktualisieren:

 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); }); });

Wenn Sie den Test ausführen, sehen Sie, dass die hinzugefügten Tests fehlschlagen, da wir die TimerButton- Komponente noch nicht aktualisiert haben. Aktualisieren wir die TimerButton -Komponente, um das Click-Ereignis hinzuzufügen:

 const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );

Jetzt sollten die Tests bestanden werden.

Als Nächstes fügen wir weitere Tests hinzu, um den Zustand zu überprüfen, wenn jede Funktion im gemounteten Timer -Testfall aufgerufen wird:

 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); });

Wenn Sie die Tests ausführen, werden sie fehlschlagen, da wir noch nicht jede Methode implementiert haben. Lassen Sie uns also jede Funktion implementieren, um die Tests zu bestehen:

 startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }

Sie werden sehen, dass die Tests bestanden werden, wenn Sie sie ausführen. Lassen Sie uns nun die verbleibenden Funktionen in Timer.jsx implementieren :

 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;

Sie werden sehen, dass alle Funktionen basierend auf den zuvor vorbereiteten User Stories funktionieren.

Timer

So haben wir also eine einfache React-App mit TDD entwickelt. Wenn die User Stories und die Akzeptanzkriterien detaillierter sind, können die Testfälle präziser geschrieben werden und somit noch mehr beitragen.

Einpacken

Bei der Entwicklung einer Anwendung mit TDD ist es sehr wichtig, das Projekt nicht nur in Epics oder User Stories herunterzubrechen, sondern sich auch gut auf Akzeptanzkriterien vorzubereiten. In diesem Artikel wollte ich Ihnen zeigen, wie Sie das Projekt aufschlüsseln und die vorbereiteten Akzeptanzkriterien für die Entwicklung von React TDD verwenden können.

Obwohl es viele Ressourcen zu React TDD gibt, hoffe ich, dass dieser Artikel Ihnen dabei geholfen hat, mithilfe von User Stories ein wenig über die TDD-Entwicklung mit React zu lernen. Sollten Sie sich für diesen Ansatz entscheiden, finden Sie hier den vollständigen Quellcode.