Rozwój oparty na testach React: od historii użytkowników do produkcji
Opublikowany: 2022-03-11W tym poście zamierzamy opracować aplikację React za pomocą programowania opartego na testach (TDD) od historyjek użytkownika do programowania. Ponadto będziemy używać Jest i Enzyme dla TDD. Po ukończeniu tego przewodnika będziesz mógł:
- Twórz eposy i historie użytkowników na podstawie wymagań.
- Twórz testy na podstawie historyjek użytkowników.
- Opracuj aplikację React za pomocą TDD.
- Użyj Enzyme i Jest, aby przetestować aplikację React.
- Użyj/ponownie wykorzystaj zmienne CSS do projektowania responsywnego.
- Utwórz komponent React wielokrotnego użytku, który renderuje i działa inaczej w oparciu o dostarczone rekwizyty.
- Wpisz właściwości kontrolne komponentu check za pomocą React PropTypes.
W tym artykule zakładamy, że masz podstawową wiedzę na temat Reacta. Jeśli jesteś zupełnie nowy w React, polecam ukończenie oficjalnego samouczka i zapoznanie się z samouczkiem React 2019 Toptal: część 1 i część 2.
Przegląd naszej testowanej aplikacji React
Będziemy budować podstawową aplikację czasomierza pomodoro, składającą się z kilku elementów interfejsu użytkownika. Każdy komponent będzie miał osobny zestaw testów w odpowiednim pliku testowym. Po pierwsze, moglibyśmy tworzyć eposy i historyjki użytkowników w następujący sposób, w oparciu o nasze wymagania projektowe.
EPICKI | HISTORIA UŻYTKOWNIKA | KRYTERIA PRZYJĘCIA |
Jako użytkownik muszę korzystać z timera, aby móc zarządzać swoim czasem. | Jako użytkownik muszę uruchomić minutnik, aby móc odliczać czas. | Upewnij się, że użytkownik jest w stanie: *uruchom minutnik *zobacz, jak timer zaczyna odliczać Odliczanie czasu nie powinno być przerywane, nawet jeśli użytkownik kliknie przycisk start więcej niż jeden raz. |
Jako użytkownik muszę zatrzymać stoper, aby móc odliczać czas tylko w razie potrzeby. | Upewnij się, że użytkownik jest w stanie: *zatrzymaj minutnik *zobacz stoper zatrzymany Nic nie powinno się zdarzyć, nawet jeśli użytkownik kliknie przycisk stop więcej niż raz. | |
Jako użytkownik muszę zresetować minutnik, aby móc odliczać czas od początku. | Upewnij się, że użytkownik jest w stanie: *zresetuj timer *zobacz resetowanie timera do ustawień domyślnych |
Szkielet
Konfiguracja projektu
Najpierw stworzymy projekt React za pomocą aplikacji Create React w następujący sposób:
$ npx create-react-app react-timer $ cd react-timer $ npm start
Zobaczysz nową kartę przeglądarki otwartą pod adresem URL http://localhost:3000. Możesz zatrzymać działającą aplikację React za pomocą Ctrl+C .
Teraz dodamy Jest i Enzyme oraz kilka zależności w następujący sposób:
$ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16
Ponadto dodamy lub zaktualizujemy plik o nazwie setupTests.js w katalogu src :
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Ponieważ Create React App uruchamia plik setupTests.js przed każdym testem, wykona i poprawnie skonfiguruje Enzyme.
Konfiguracja CSS
Zamierzamy napisać zmienne i podstawowy reset CSS, ponieważ chcemy, aby zmienne CSS były globalnie dostępne w aplikacji. Zdefiniujemy zmienne z zakresu :root. Składnia definiowania zmiennych polega na użyciu niestandardowej notacji właściwości, z których każda zaczyna się od –, po którym następuje nazwa zmiennej.
Przejdź do pliku index.css i dodaj następujące elementy:
:root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }
Teraz musimy zaimportować CSS do naszej aplikacji. Zaktualizuj plik index.js w następujący sposób:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )
Test płytkiego renderowania
Jak być może już wiesz, proces TDD wyglądałby tak:
- Dodaj test.
- Uruchom wszystkie testy, a zobaczysz, że test się nie powiedzie.
- Napisz kod, aby zdać test.
- Uruchom wszystkie testy.
- Refaktoryzacja.
- Powtarzać.
Dlatego dodamy pierwszy test dla testu płytkiego renderowania, a następnie napiszemy kod, który przejdzie test. Dodaj nowy plik specyfikacji o nazwie App.spec.js do katalogu src/components/App w następujący sposób:
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); }); });
Następnie możesz uruchomić test:
$ npm test
Zobaczysz, że test się nie powiedzie.
Komponent aplikacji
Teraz przejdziemy do tworzenia komponentu App, aby przejść test. Przejdź do App.jsx w katalogu src/components/App i dodaj kod w następujący sposób:
import React from 'react'; const App = () => <div className=”app-container” />; export default App;
Teraz uruchom test ponownie.
$ npm test
Pierwszy test powinien teraz zaliczyć.
Dodawanie CSS aplikacji
Utworzymy plik App.css w katalogu src/components/App , aby dodać styl do komponentu App w następujący sposób:
.app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }
Teraz jesteśmy gotowi do zaimportowania CSS do pliku App.jsx :
import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;
Następnie musimy zaktualizować plik index.js , aby zaimportować komponent App w następujący sposób:
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()
Dodawanie składnika czasowego
Wreszcie aplikacja będzie zawierać składnik Timer, dlatego zamierzamy zaktualizować plik App.spec.js , aby sprawdzić obecność składnika Timer w naszej aplikacji. Ponadto zamierzamy zadeklarować zmienną kontenera poza pierwszym przypadkiem testowym, ponieważ test płytkiego renderowania należy wykonać przed każdym przypadkiem testowym.
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) }) })
Jeśli uruchomisz npm test
na tym etapie, test zakończy się niepowodzeniem, ponieważ składnik Timer jeszcze nie istnieje.
Pisanie testu płytkiego renderowania timera
Teraz utworzymy plik o nazwie Timer.spec.js w nowym katalogu o nazwie Timer w katalogu src/components .
Dodamy również test płytkiego renderowania w pliku 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) }) })
Test zakończy się niepowodzeniem, zgodnie z oczekiwaniami.
Tworzenie komponentu czasowego
Następnie utwórzmy nowy plik o nazwie Timer.jsx i zdefiniujmy te same zmienne i metody na podstawie historyjek użytkownika:
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;
To powinno przejść test i powinno wyrenderować <div />
w pliku Timer.spec.js , ale test nie powinien renderować komponentu Timer, ponieważ nie dodaliśmy jeszcze komponentu Timer w komponencie aplikacji.
Dodamy składnik Timer w pliku App.jsx w następujący sposób:
import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;
Wszystkie testy powinny teraz zaliczyć.
Dodawanie CSS timera
Dodamy zmienne CSS związane z Timerem i dodamy zapytania o media dla mniejszych urządzeń.
Zaktualizuj plik index.css w następujący sposób:
: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%; } }
Ponadto utworzymy plik Timer.css w katalogu components/Timer :
.timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }
Musimy zaktualizować Timer.jsx , aby zaimportować plik Timer.css .

import React, { Component } from "react" import "./Timer.css"
Jeśli uruchomisz teraz aplikację React, w przeglądarce zobaczysz prosty ekran z ramką.
Napisz test płytkiego renderowania TimerButton
Potrzebujemy trzech przycisków: Start, Stop i Reset , dlatego utworzymy komponent TimerButton .
Najpierw musimy zaktualizować plik Timer.spec.js , aby sprawdzić istnienie komponentu TimerButton w komponencie Timer :
it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })
Teraz dodajmy plik TimerButton.spec.js do nowego katalogu o nazwie TimerButton w katalogu src/components i dodajmy test do pliku w następujący sposób:
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) }) })
Teraz, jeśli uruchomisz test, zobaczysz, że test się nie powiedzie.
Stwórzmy plik TimerButton.jsx dla komponentu 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;
Jeśli uruchomisz npm test
na tym etapie, test powinien wyrenderować wystąpienia składnika TimerButton, ale zakończy się niepowodzeniem, ponieważ nie dodaliśmy jeszcze składników TimerButton do składnika Timer.
Zaimportujmy składnik TimerButton i dodajmy trzy składniki TimerButton w metodzie render w 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> ); };
CSS przycisku Timer
Teraz nadszedł czas na dodanie zmiennych CSS dla składnika TimerButton. Dodajmy zmienne z zakresu :root do pliku index.css :
:root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }
Stwórzmy również plik o nazwie TimerButton.css w katalogu TimerButton w katalogu 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; }
Zaktualizujmy odpowiednio TimerButton.jsx , aby zaimportować plik TimerButton.css i wyświetlić wartość przycisku:
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;
Ponadto musimy zaktualizować Timer.css , aby wyrównać trzy przyciski w poziomie, więc zaktualizujmy również plik 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;
Jeśli uruchomisz teraz aplikację React, zobaczysz następujący ekran:

Refaktoryzacja timera
Zamierzamy dokonać refaktoryzacji Timera, ponieważ chcemy zaimplementować funkcje takie jak startTimer, stopTimer, restartTimer i resetTimer . Zaktualizujmy najpierw plik 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); }); });
Jeśli uruchomisz test, zobaczysz, że dodane testy nie powiodły się, ponieważ nie zaktualizowaliśmy jeszcze składnika TimerButton . Zaktualizujmy komponent TimerButton , aby dodać zdarzenie kliknięcia:
const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );
Teraz testy powinny zdać.
Następnie dodamy więcej testów, aby sprawdzić stan wywołania każdej funkcji w zamontowanym przypadku testowym Timer :
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); });
Jeśli uruchomisz testy, zobaczysz, że się nie powiodą, ponieważ nie zaimplementowaliśmy jeszcze każdej metody. Zaimplementujmy więc każdą funkcję, aby przejść testy:
startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }
Zobaczysz, że testy przejdą, jeśli je uruchomisz. Teraz zaimplementujmy pozostałe funkcje w 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;
Zobaczysz, że wszystkie funkcje działają w oparciu o przygotowane wcześniej przez nas historyjki użytkowników.
W ten sposób opracowaliśmy podstawową aplikację React przy użyciu TDD. Jeśli historyjki użytkownika i kryteria akceptacji są bardziej szczegółowe, przypadki testowe można pisać bardziej precyzyjnie, przyczyniając się w ten sposób do jeszcze większego wkładu.
Zawijanie
Podczas tworzenia aplikacji przy użyciu TDD bardzo ważne jest nie tylko rozbicie projektu na epopeje czy historyjki użytkownika, ale także dobre przygotowanie się do kryteriów akceptacji. W tym artykule chciałem pokazać jak rozbić projekt i wykorzystać przygotowane kryteria akceptacji dla rozwoju React TDD.
Mimo że istnieje wiele zasobów związanych z React TDD, mam nadzieję, że ten artykuł pomógł ci dowiedzieć się trochę o rozwoju TDD za pomocą Reacta przy użyciu historyjek użytkowników. Jeśli zdecydujesz się na emulację tego podejścia, zapoznaj się z pełnym kodem źródłowym tutaj.