Разработка через тестирование в React: от пользовательских историй к производству
Опубликовано: 2022-03-11В этом посте мы собираемся разработать приложение React, используя разработку через тестирование (TDD) от пользовательских историй до разработки. Кроме того, мы собираемся использовать Jest и Enzyme для TDD. По завершении этого руководства вы сможете:
- Создавайте эпики и пользовательские истории на основе требований.
- Создавайте тесты на основе пользовательских историй.
- Разработайте приложение React, используя TDD.
- Используйте Enzyme и Jest для тестирования приложения React.
- Используйте/повторно используйте переменные CSS для адаптивного дизайна.
- Создайте многоразовый компонент React, который отображается и функционирует по-разному в зависимости от предоставленных реквизитов.
- Введите реквизиты проверки компонентов, используя React PropTypes.
В этой статье предполагается, что у вас есть базовые знания React. Если вы новичок в React, я бы порекомендовал вам пройти официальный учебник и взглянуть на учебник по React от Toptal 2019: часть 1 и часть 2.
Обзор нашего тестируемого приложения React
Мы создадим базовое приложение-таймер Pomodoro, состоящее из некоторых компонентов пользовательского интерфейса. Каждый компонент будет иметь отдельный набор тестов в соответствующем тестовом файле. Прежде всего, мы могли бы создавать эпики и пользовательские истории следующим образом, исходя из требований нашего проекта.
ЭПИЧЕСКИЙ | ИСТОРИЯ ПОЛЬЗОВАТЕЛЯ | КРИТЕРИИ ПРИЕМКИ |
Как пользователю, мне нужно использовать таймер, чтобы я мог управлять своим временем. | Как пользователь, мне нужно запустить таймер, чтобы я мог отсчитывать свое время. | Убедитесь, что пользователь может: *запустить таймер * смотрите, как таймер начинает обратный отсчет Отсчет времени не должен прерываться, даже если пользователь нажимает кнопку запуска более одного раза. |
Как пользователю, мне нужно остановить таймер, чтобы я мог отсчитывать свое время только тогда, когда это необходимо. | Убедитесь, что пользователь может: * остановить таймер * см. таймер остановился Ничего не должно произойти, даже если пользователь нажмет кнопку остановки более одного раза. | |
Как пользователю, мне нужно сбросить таймер, чтобы я мог отсчитывать время с самого начала. | Убедитесь, что пользователь может: *сбросить таймер * см. сброс таймера на значение по умолчанию |
Каркас
Настройка проекта
Во-первых, мы собираемся создать проект React с помощью Create React App следующим образом:
$ npx create-react-app react-timer $ cd react-timer $ npm start
Вы увидите новую вкладку браузера, открытую по адресу http://localhost:3000. Вы можете остановить запущенное приложение React с помощью Ctrl+C .
Теперь мы собираемся добавить Jest и Enzyme и некоторые зависимости следующим образом:
$ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16
Кроме того, мы добавим или обновим файл с именем setupTests.js в каталоге src :
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Поскольку приложение Create React запускает файл setupTests.js перед каждым тестом, оно будет выполнять и правильно настраивать Enzyme.
Настройка CSS
Мы собираемся написать переменные и базовый сброс CSS, потому что мы хотим, чтобы переменные CSS были глобально доступны в приложении. Мы определим переменные из области :root. Синтаксис для определения переменных заключается в использовании обозначения пользовательского свойства, каждое из которых начинается с -, за которым следует имя переменной.
Перейдите к файлу index.css и добавьте следующее:
:root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }
Теперь нам нужно импортировать CSS в наше приложение. Обновите файл index.js следующим образом:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )
Поверхностный тест рендеринга
Как вы, возможно, уже знаете, процесс TDD будет выглядеть следующим образом:
- Добавьте тест.
- Запустите все тесты, и вы увидите, что тест не пройден.
- Напишите код для прохождения теста.
- Проведите все тесты.
- Рефакторинг.
- Повторить.
Следовательно, мы добавим первый тест для поверхностного рендеринга, а затем напишем код для прохождения теста. Добавьте новый файл спецификации с именем App.spec.js в каталог src/components/App следующим образом:
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); }); });
Затем вы можете запустить тест:
$ npm test
Вы увидите, что тест не пройден.
Компонент приложения
Теперь мы приступим к созданию компонента приложения для прохождения теста. Перейдите к App.jsx в каталоге src/components/App и добавьте следующий код:
import React from 'react'; const App = () => <div className=”app-container” />; export default App;
Теперь снова запустите тест.
$ npm test
Теперь должен пройти первый тест.
Добавление CSS приложения
Мы собираемся создать файл App.css в каталоге src/components/App , чтобы добавить стиль к компоненту App следующим образом:
.app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }
Теперь мы готовы импортировать CSS в файл App.jsx :
import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;
Далее нам нужно обновить файл index.js , чтобы импортировать компонент приложения следующим образом:
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()
Добавление компонента таймера
Наконец, приложение будет содержать компонент Timer, поэтому мы собираемся обновить файл App.spec.js , чтобы проверить наличие компонента Timer в нашем приложении. Кроме того, мы собираемся объявить переменную контейнера за пределами первого тестового примера, поскольку перед каждым тестовым случаем необходимо выполнять тест поверхностного рендеринга.
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) }) })
Если вы запустите npm test
на этом этапе, тест завершится ошибкой, поскольку компонент Timer еще не существует.
Написание теста рендеринга Timer Shallow
Теперь мы собираемся создать файл с именем Timer.spec.js в новом каталоге с именем Timer в каталоге src/components .
Также мы добавим тест поверхностного рендеринга в файл 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) }) })
Тест провалится, как и ожидалось.
Создание компонента таймера
Далее давайте создадим новый файл с именем Timer.jsx и определим те же переменные и методы на основе пользовательских историй:
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;
Это должно пройти тест и должно отображать <div />
в файле Timer.spec.js , но тест не должен отображать компонент таймера, поскольку мы еще не добавили компонент таймера в компонент приложения.
Мы собираемся добавить компонент Timer в файл App.jsx следующим образом:
import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;
Все тесты должны пройти сейчас.
Добавление таймера CSS
Мы собираемся добавить переменные CSS, связанные с таймером, и добавить медиа-запросы для небольших устройств.
Обновите файл index.css следующим образом:
: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%; } }
Кроме того, мы собираемся создать файл Timer.css в каталоге components/Timer :
.timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }
Нам нужно обновить Timer.jsx , чтобы импортировать файл Timer.css .

import React, { Component } from "react" import "./Timer.css"
Если вы сейчас запустите приложение React, вы увидите простой экран с рамкой в своем браузере.
Напишите тест поверхностного рендеринга TimerButton
Нам нужны три кнопки: Start, Stop и Reset , поэтому мы создадим компонент TimerButton .
Во-первых, нам нужно обновить файл Timer.spec.js , чтобы проверить наличие компонента TimerButton в компоненте Timer :
it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })
Теперь давайте добавим файл TimerButton.spec.js в новый каталог с именем TimerButton в каталоге src/components и добавим тест в файл следующим образом:
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) }) })
Теперь, если вы запустите тест, вы увидите, что тест не пройден.
Создадим файл TimerButton.jsx для компонента 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;
Если вы запустите npm test
на этом этапе, тест должен отобразить экземпляры компонента TimerButton, но завершится ошибкой, поскольку мы еще не добавили компоненты TimerButton в компонент Timer.
Давайте импортируем компонент TimerButton и добавим три компонента TimerButton в метод рендеринга в 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
Теперь пришло время добавить переменные CSS для компонента TimerButton. Давайте добавим переменные из области :root в файл index.css :
:root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }
Кроме того, давайте создадим файл с именем TimerButton.css в каталоге TimerButton в каталоге 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; }
Давайте соответствующим образом обновим TimerButton.jsx , чтобы импортировать файл TimerButton.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;
Кроме того, нам нужно обновить Timer.css , чтобы выровнять три кнопки по горизонтали, поэтому давайте также обновим файл 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;
Если вы сейчас запустите приложение React, вы увидите следующий экран:

Рефакторинг таймера
Мы собираемся реорганизовать Timer, так как мы хотим реализовать такие функции, как startTimer, stopTimer, restartTimer и resetTimer . Давайте сначала обновим файл 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); }); });
Если вы запустите тест, вы увидите, что добавленные тесты завершатся неудачей, поскольку мы еще не обновили компонент TimerButton . Обновим компонент TimerButton, чтобы добавить событие клика:
const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );
Теперь тесты должны пройти.
Далее мы собираемся добавить дополнительные тесты для проверки состояния при вызове каждой функции в смонтированном тестовом случае 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); });
Если вы запустите тесты, вы увидите, что они потерпят неудачу, поскольку мы еще не реализовали каждый метод. Итак, давайте реализуем каждую функцию для прохождения тестов:
startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }
Вы увидите, что тесты пройдены, если вы их запустите. Теперь давайте реализуем оставшиеся функции в 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;
Вы увидите, как все функции работают на основе пользовательских историй, которые мы подготовили ранее.
Итак, вот как мы разработали базовое приложение React с использованием TDD. Если пользовательские истории и критерии приемлемости будут более подробными, тестовые примеры могут быть написаны более точно, что внесет еще больший вклад.
Подведение итогов
При разработке приложения с использованием TDD очень важно не только разбить проект на эпики или пользовательские истории, но и хорошо подготовиться к критериям приемки. В этой статье я хотел показать вам, как разбить проект и использовать подготовленные критерии приемки для разработки React TDD.
Несмотря на то, что существует множество ресурсов, связанных с React TDD, я надеюсь, что эта статья помогла вам немного узнать о разработке TDD с помощью React с использованием пользовательских историй. Если вы решите эмулировать этот подход, обратитесь к полному исходному коду здесь.