التطوير القائم على اختبار التفاعل: من قصص المستخدم إلى الإنتاج

نشرت: 2022-03-11

في هذا المنشور ، سنطور تطبيق React باستخدام التطوير القائم على الاختبار (TDD) من قصص المستخدمين إلى التطوير. أيضًا ، سنستخدم Jest and Enzyme لـ TDD. عند الانتهاء من هذا الدليل ، ستتمكن من:

  • قم بإنشاء ملاحم وقصص مستخدم بناءً على المتطلبات.
  • قم بإنشاء اختبارات بناءً على قصص المستخدم.
  • قم بتطوير تطبيق React باستخدام TDD.
  • استخدم Enzyme and Jest لاختبار تطبيق React.
  • استخدم / أعد استخدام متغيرات CSS للتصميم سريع الاستجابة.
  • أنشئ مكوِّن React قابلًا لإعادة الاستخدام والذي يعرض ويعمل بشكل مختلف بناءً على الخاصيات المتوفرة.
  • اكتب دعائم مكون التحقق باستخدام React PropTypes.

تفترض هذه المقالة أن لديك معرفة أساسية بـ React. إذا كنت جديدًا تمامًا على React ، فإنني أوصيك بإكمال البرنامج التعليمي الرسمي وإلقاء نظرة على برنامج Toptal's 2019 React Tutorial: الجزء 1 والجزء 2.

نظرة عامة على تطبيق React المستند إلى الاختبار

سننشئ تطبيق مؤقت بومودورو أساسي يتكون من بعض مكونات واجهة المستخدم. سيكون لكل مكون مجموعة منفصلة من الاختبارات في ملف اختبار مطابق. بادئ ذي بدء ، يمكننا إنشاء ملاحم وقصص مستخدمين على النحو التالي بناءً على متطلبات مشروعنا.

الملحم قصة المستخدم معايير القبول
كمستخدم ، أحتاج إلى استخدام المؤقت حتى أتمكن من إدارة وقتي. بصفتي مستخدمًا ، أحتاج إلى بدء تشغيل المؤقت حتى أتمكن من العد التنازلي لوقتي. تأكد من أن المستخدم قادر على:

* ابدأ الموقت
* انظر الموقت يبدأ العد التنازلي

لا ينبغي مقاطعة العد التنازلي للوقت حتى إذا نقر المستخدم على زر البدء أكثر من مرة.
بصفتي مستخدمًا ، أحتاج إلى إيقاف المؤقت حتى لا يمكنني العد التنازلي لوقتي إلا عند الحاجة. تأكد من أن المستخدم قادر على:

* اوقف الموقت
* انظر الموقت توقف

لا يجب أن يحدث أي شيء حتى إذا نقر المستخدم على زر الإيقاف أكثر من مرة.
بصفتي مستخدمًا ، أحتاج إلى إعادة ضبط المؤقت حتى أتمكن من العد التنازلي لوقتي من البداية. تأكد من أن المستخدم قادر على:

* إعادة ضبط الموقت
* راجع إعادة ضبط الموقت إلى الوضع الافتراضي

إطار سلكي

إطار سلكي

إعداد مشروع

أولاً ، سننشئ مشروع React باستخدام تطبيق Create React على النحو التالي:

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

سترى علامة تبويب متصفح جديدة مفتوحة على عنوان URL 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 App يقوم بتشغيل ملف setupTests.js قبل كل اختبار ، فإنه سيقوم بتنفيذ Enzyme وتكوينه بشكل صحيح.

تكوين CSS

سنقوم بكتابة المتغيرات وإعادة تعيين CSS الأساسية لأننا نريد متغيرات CSS متاحة عالميًا في التطبيق. سنحدد المتغيرات من: نطاق الجذر. بناء الجملة لتعريف المتغيرات هو استخدام تدوين خاص للخاصية المخصصة ، كل منها يبدأ بـ - متبوعًا باسم المتغير.

انتقل إلى ملف 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 كما يلي:

  1. أضف اختبارًا.
  2. قم بإجراء جميع الاختبارات ، وسترى فشل الاختبار.
  3. اكتب الكود لاجتياز الاختبار.
  4. قم بإجراء جميع الاختبارات.
  5. مُعاد تصنيعه.
  6. يكرر.

ومن ثم ، سنضيف الاختبار الأول لاختبار تصيير ضحل ثم نكتب الكود لاجتياز الاختبار. أضف ملف مواصفات جديد باسم 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-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.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 نظرًا لأننا لم نقم بإضافة مكون Timer في مكون التطبيق حتى الآن.

سنقوم بإضافة مكون 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;

يجب أن تمر جميع الاختبارات الآن.

مضيفا Timer 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 ضمن مكونات الدليل / 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 Shallow Rendering

نحتاج إلى ثلاثة أزرار: ابدأ ، وإيقاف ، وإعادة تعيين ، ومن ثم سنقوم بإنشاء مكون 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> ); };

TimerButton CSS

حان الوقت الآن لإضافة متغيرات CSS لمكون TimerButton. دعنا نضيف متغيرات في : نطاق الجذر إلى ملف 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 / المكونات :

 .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 الآن ، فسترى شاشة على النحو التالي:

الموقت

إعادة هيكلة الموقت

سنقوم بإعادة تشكيل المؤقت لأننا نريد تنفيذ وظائف مثل startTimer ، و stopTimer ، و resetTimer ، و 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 باستخدام قصص المستخدم. إذا اخترت محاكاة هذا الأسلوب ، فيرجى الرجوع إلى كود المصدر الكامل هنا.