به همین جهت اگر میخواهید رزومهی غنیتری را ارائه دهید، فراگیری React میتواند موقعیتهای شغلی بیشتری را نصیب شما کند.
ساختار کلی یک برنامهی React
کامپوننتها (جزئی از یک رابط کاربری) قلب هر برنامهی React ای را تشکیل میدهند. برای ساخت یک برنامهی React، تعدادی کامپوننت مستقل را تهیه و با هم ترکیب میکنیم تا به رابط کاربری نهایی برسیم.
هر برنامهی React، حداقل از یک کامپوننت تشکیل میشود که به آن Root component هم میگویند. این کامپوننت بیانگر کل برنامهاست و دربرگیرندهی مابقی Child components برنامه است. بنابراین ساختار هر برنامهی React، شبیه به درختی از کامپوننتها است. اگر با Angular 2 به بعد کار کرده باشید، این مفهوم برای شما آشنا است.
یک مثال: فرض کنید میخواهیم UI برنامهای را به مانند رابط کاربری Twitter، ایجاد کنیم. هر قسمت یک صفحهی توئیتر، به کامپوننتهایی شکسته میشود؛ مانند منوی راهبری، نمایش پروفایل شخص، نمایش لیست آخرین اخبار مورد علاقهی شخص و نمایش فید. اگر بخواهیم این ساختار را توسط یک برنامهی React شبیه سازی کنیم، در بالاترین سطح، کامپوننت root را خواهیم داشت که کار ترکیب و نمایش سایر کامپوننتهای برنامه مانند nav bar ، trends ، profile و feed را انجام میدهد. اکنون در این ساختار ایجاد شده، برای مثال کامپوننت feed نیز میتواند از چندین کامپوننت مجزا تشکیل شود؛ مانند کامپوننتهای tweet و like.
بنابراین هر کامپوننت، قسمتی از UI را تشکیل میدهد. هر کدام از آنها به صورت مجزای از دیگری ساخته شده و سپس در کنار هم قرار میگیرند تا UI نهایی را شکل دهند:
هر کامپوننت در React به صورت یک کلاس ES6، با ساختاری که دارای یک شیء state و متد render است، تشکیل میشود:
class Tweet { state = {}; render() { } }
مزیت کارکردن با Virtual DOM، سادگی ایجاد، تغییر و به روز رسانی آن در مقایسه با DOM واقعی است که در نهایت کار رندر عناصر UI را در مرورگر انجام میدهد. زمانیکه در state کامپوننتی تغییری رخ میدهد، یک React Element جدید تولید میشود. سپس React این شیء جدید را با نمونهی قبلی آن مقایسه کرده و تغییرات رخداده را محاسبه میکند. در آخر این تغییرات را به DOM واقعی اعمال میکند تا با Virtual DOM موجود هماهنگ شود.
بنابراین در حین کار با React، دیگر همانند کار با جاوا اسکریپت خالص و یا jQuery، مستقیما عناصر UI و DOM واقعی را تغییر نمیدهیم. در اینجا فقط state یک کامپوننت را تغییر میدهیم و سپس React، کار ایجاد شیء UI درون حافظهای متناظر با آن و سپس اعمال آنرا به UI نهایی قابل مشاهدهی در مرورگر، انجام میدهد. به همین جهت به این کتابخانه React میگویند! چون به تغییرات state کامپوننتها واکنش نشان میدهد و سپس DOM واقعی را به روز میکند.
Angular یا React؟!
هر دوی React و Angular از لحاظ طراحی کامپوننتها بسیار شبیه به هم هستند؛ اما Angular یک فریمورک است و React تنها یک کتابخانه. تنها کاری را که React انجام میدهد، رندر View است و هماهنگ نگه داشتن آن با state کامپوننتها. این تمام کاری است که React انجام میدهد؛ نه بیشتر و نه کمتر! بنابراین یادگیری React، بسیار سریعتر و سادهتر از Angular است. بدیهی است یک برنامهی تک صفحهای وب، از اجزای دیگری مانند مسیریابی و یا کار با سرویسهای HTTP نیز تشکیل میشود. در React شما مختار هستید که کتابخانههای جانبی فراهم شدهی برای آنرا خودتان انتخاب کرده و استفاده کنید؛ برخلاف روشی که در Angular مرسوم است و به صورت مشخص و ثابتی به همراه این فریمورک ارائه میشوند.
برپایی محیط توسعهی React
اولین برنامهای را که برای کار با React باید نصب کنید، node.js است. البته ما در این سری قرار نیست با node.js کار کنیم؛ اما از یکی از اجزای آن به نام node package manager یا npm، برای نصب کتابخانهی جاوا اسکریپتی ثالث، زیاد استفاده خواهیم کرد. پس از نصب آن، به خط فرمان مراجعه کرد و دستور زیر را صادر کنید:
> npm install -g npm@latest
اگر هم خیلی پیشترها node.js را نصب کردهاید (برای مثال چند سال قبل!)، نصب نگارش جدید آن احتمالا کار نخواهد کرد. حتی عزل و نصب مجدد آن نیز کارساز نیست. در این حالت باید پس از عزل آن، پوشههای قدیمی آنرا یکی یکی یافته و دستی حذف کنید . سپس مجددا آنرا نصب کنید.
در ادامه در خط فرمان و توسط npm، قالب create-react-app را نصب خواهیم کرد:
> npm i -g create-react-app
ابزار دیگری که در این سری از آن استفاده خواهیم کرد، ادیتور بسیار معروف و محبوب VSCode است. پس از دریافت و نصب آن، چند افزونهی زیر را نیز به آن اضافه خواهیم کرد:
برای نصب آنها، پنل extensions را در VSCode، از نوار ابزار کنار صفحهی آن، انتخاب کرده و نامهای فوق را در آن جستجو و سپس نصب کنید.
و یا میتوانید این فایل را اجرا کرده و تعدادی از افزونههای مفید VSCode را یکجا نصب کنید: install-addons.zip
همچنین قابلیت فرمتکردن پس از Save را نیز در VSCode فعال کنید تا پس از هربار Save، اعمال این افزونهها به صورت خودکار صورت گیرد. برای این منظور گزینهی file->preferences->settings را در VSCode انتخاب کرده و سپس save را جستجو کرده و Format On Save را انتخاب کنید:
علاوه بر اینها، جهت کار بهتر با VSCode، بهتر است بررسی کنندههای کدهای جاوا اسکریپتی (static code analyzers) را نیز با اجرای دستور زیر نصب کنید:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks
پس از این تغییرات، نیاز است یکبار VSCode را بسته و مجددا باز کنید. سپس مجددا گزینهی file->preferences->settings را در VSCode انتخاب کرده و ابتدا eslint را در اینجا جستجو کنید. در صفحهی نمایش تنظیمات آن، گزینهی Auto fix on save آنرا انتخاب نمائید. در آخر در همین قسمت settings، عبارت prettier را انتخاب کنید. در اینجا اگر گزینهی قدیمی یکپارچگی با eslint آن هنوز وجود دارد، آنرا از حالت انتخاب شده خارج کنید (به صورت قرمز و deprecated نمایش داده میشود) تا افزونهی prettier بدون مشکل و خطا کار کند (disable Prettier ESLint integration).
ایجاد قالب اولین برنامهی React
در ادامه برای ایجاد اولین برنامهی React، از بستهی create-react-app که پیشتر آنرا نصب کردیم، استفاده میکنیم. برای این منظور در خط فرمان دستور زیر را صادر کنید:
> create-react-app sample-01
این قالب نه تنها React را نصب میکند، بلکه یک development server را برای اجرا و مشاهدهی سریع برنامه، webpack را برای یکی کردن فایلها (bundling & minification)، Babel را برای کامپایل کدهای فایلهای JSX و ... نیز نصب میکند. بنابراین به این ترتیب، یک پروژهی تنظیم شده و آمادهی استفاده و توسعه را شاهد خواهیم بود که نیازی به تنظیمات اولیهی آن نیست.
پس ایجاد برنامه، وارد پوشهی sample-01 شده و دستور npm start را صادر کنید:
> cd sample-01 > npm start
development server آن، تغییرات فایلهای برنامه را تحت نظر قرار میدهد و با هر تغییری، به صورت خودکار برنامه را در مرورگر بارگذاری مجدد خواهد کرد.
بررسی ساختار اولین پروژهی React ایجاد شده
ساختار پوشهها و فایلهای مثال اولیهی ایجاد شده توسط قالب create-react-app به صورت زیر است:
البته شما در این تصویر پوشهی node_modules را که در کنار این پوشهها قرار دارد، مشاهده نمیکنید. وجود یک چنین پوشهی سنگینی با هزاران فایل داخل آن، کار نمایشی IDEها را با مشکل مواجه میکند (مصرف حافظهی بالا، به همراه کند شدن شدید آن). اگر نمیخواهید این پوشه نمایش داده شود، در مسیر file->preferences->settings، عبارت npm را جستجو کرده و سپس در قسمت npm: exclude آن، بر روی لینک edit in settings.json کلیک کنید:
و سپس در فایل باز شده، یک چنین تنظیمی را میتوانید اضافه و یا ویرایش و تکمیل کنید:
"files.exclude": { "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, "**/node_modules": true, "**/wwwroot": true, "**/bower_components": true, "**/**/bin": true, "**/**/obj": true, "**/packages": true },
در ادامه پوشهی public این پروژه را مشاهده میکنید. تمام فایلهایی که قرار است به صورت عمومی توسط برنامه ارائه شوند، مانند favicon.ico و غیره، در این پوشه قرار میگیرند.
در این پوشه بر روی فایل index.html آن کلیک کنید تا بتوان محتوای آنرا بهتر بررسی کرد. برای مثال در ابتدای آن، درج تعدادی متادیتا را که یکی از آنها ذکر manifest.json است، مشاهده میکنید. کار فایل manifest.json، ارائهی یک سری متادیتای خاص مخصوص دستگاههای موبایل است که در آنها بجای favicon.ico، میتوان از تصاویر و یا آیکنهای بزرگتری مانند فایلهای png موجود در پوشهی public، استفاده کرد. در ادامهی این فایل، به تنظیم زیر میرسیم:
<body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div>
در پوشهی src و فایل App.js آن، شاهد یک کامپوننت ابتدایی هستید که کار رندر صفحهی مشکی پیشفرض این قالب را انجام میدهد. در این فایل، شاهد بازگشت یک چنین تگهایی هستیم:
return ( <div className="App"> <header className="App-header"> ... </header> </div> );
برای درک بهتر آن به آدرس https://babeljs.io/repl مراجعه کنید. سپس در سمت چپ صفحه، یک قطعه کد jsx را به یک ثابت انتساب دهید:
const element = <h1>Hello World!</h1>;
همانطور که مشاهده میکنید، این قطعه کد jsx (که یک رشتهی معمولی نیست)، توسط Babel به یک قطعه کد کاملا جاوا اسکریپتی قابل درک برای مرورگر تبدیل شدهاست:
"use strict"; var element = React.createElement("h1", null, "Hello World!");
بدیهی است نوشتن کدهای jsx، سادهتر از نوشتن قطعه کد فوق است و درک آن نیز به علت شباهت آن به HTML، آسانتر است. به همین جهت در کدهای React، ما از jsx استفاده میکنیم و تفسیر آنرا به Babel واگذار خواهیم کرد.
در پوشهی src، فایل مهم دیگری که وجود دارد، index.js است. این فایل نقطهی آغازین برنامه را مشخص میکند. در قسمتهای بعدی، محتویات این فایل را بیشتر بررسی خواهیم کرد.
در اینجا فایل serviceWorker.js را نیز مشاهده میکنید. این فایل به صورت خودکار توسط قالب create-react-app ایجاد شدهاست و کار آن کمک به ارائهی محلی برنامه، توسط development server آن است. بنابراین ما کاری با این فایل نخواهیم داشت.
نوشتن اولین برنامهی React
به پوشهی src ایجاد شده مراجعه کرده و تمام فایلهای موجود و پیشفرض آنرا حذف کنید. در ادامه خودمان آنها را از صفر ایجاد خواهیم کرد. برای این منظور فایل جدید و خالی src\index.js را ایجاد میکنیم. در ابتدای کار نیاز است تعدادی ماژول React را import کنیم.
import React from "react"; const element = <h1>Hello World!</h1>; console.log(element);
اگر هنوز برنامه توسط دستور npm start در حال اجرا است، هر بار که فایل index.js را ذخیره میکنیم، خروجی نهایی را در مرورگر نمایش میدهد (اگر هم آنرا بستهاید، یکبار از طریق خط فرمان، دستور npm start را در ریشهی پروژه، صادر کنید). به این قابلیت hot module reloading هم گفته میشود.
در این حالت اگر به مرورگر مراجعه کنید، یک صفحهی سفید را مشاهده خواهید کرد. اکنون دکمهی F12 را فشرده (و یا ctrl+shift+i) و developer console مرورگر را باز کنید.
شیءای را که در اینجا مشاهده میکنید، همان حاصل console.log کدهای فوق است؛ به عبارتی Babel، عبارت jsx ما را تبدیل به یک شیء جاوا اسکریپتی قابل فهم برای مرورگر کردهاست که از دیدگاه React، جزئی از همان Virtual DOM ای است که پیشتر معرفی شد (نمایش درون حافظهای DOM مختص React، جهت محاسبهی تغییرات، با تغییر state هر کامپوننت و سپس اعمال آنها به DOM اصلی در مرورگر).
اکنون میخواهیم این المان را در DOM اصلی، رندر کرده و نمایش دهیم:
import React from "react"; import ReactDOM from "react-dom"; const element = <h1>Hello World!</h1>; console.log(element); ReactDOM.render(element, document.getElementById("root"));
اکنون پس از ذخیره سازی فایل index.js، اگر به مرورگر مراجعه کنید، عبارت Hello World! را مشاهده خواهید کرد:
همانطور که در این تصویر نیز مشخص است، المان h1 ما را داخل div ای با id مساوی root، درج کردهاست.
هدف از این مثال ساده، نمایش نحوهی کارکرد React، در پشت صحنه بود. در یک برنامهی واقعی، بجای رندر یک المان ساده در DOM، کار رندر App component را انجام خواهیم داد. کامپوننت App، کامپوننت ریشهای برنامه بوده و میتواند شامل درختی از کامپوننتها که UI نهایی را تشکیل میدهند، شود.
نگاهی به تنظیمات پروژهی ایجاد شده
اگر فایل package.json پروژه را باز کنید، یک چنین بستههایی در آن درج شدهاست:
{ "name": "sample-01", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.11.0", "react-dom": "^16.11.0", "react-scripts": "3.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
بستهی react-scripts است که کار مدیریت چهار جزء قسمت scripts این فایل را انجام میدهد. برای نمونه دستور npm start ای که در اینجا تعریف شده، سبب اجرای react-scripts start میشود. در ادامه اگر دستور npm run build را اجرا کنیم، یک بستهی نهایی بهینه سازی شده را تولید میکند.
آخرین دستور آن eject است. اگر دستور npm run eject را اجرا کنید، امکان سفارشی سازی پشت صحنهی create-react-app را خواهید داشت؛ اما در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر میشوند). همچنین این عملیات نیز یک طرفهاست. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانیهای بعدی create-react-app را با مشکل مواجه میکند. این گزینه صرفا مختص توسعه دهندگان پیشرفتهی React است.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-01.zip
در قسمت بعد، پیشنیازهای جاوا اسکریپتی شروع به کار با React را بررسی میکنیم.
CoffeeScript #8
اصطلاحات عمومی CoffeeScript
Includes
برای چک کردن وجود یک مقدار در یک آرایه به طور معمول از indexOf استفاده میشود؛ در حالی که تمامی نسخههای IE به طور کامل از آن پشتیبانی نمیکنند.
var included = (array.indexOf("test") != -1)
included = "test" in array
included = "a long test string".indexOf("test") isnt -1
string = "a long test string" included = !~ string.indexOf "test"
تکرار Propertyها
در صورتی که به خصوصیات یک شیء چندین بار نیاز داشته باشید، در جاوااسکریپت باید از کلمهی کلیدی in استفاده کنید:
var object = {one: 1, two: 2} for(var key in object) alert(key + " = " + object[key])
object = {one: 1, two: 2} alert("#{key} = #{value}") for key, value of object
var key, object, value; object = { one: 1, two: 2 }; for (key in object) { value = object[key]; alert(key + " = " + value); }
Min/Max
درست است که این تکنیک مخصوص CoffeeScript نیست، اما اشاره به آن میتواند مفید باشد. تابع Math.max و Max.min میتوانند چندین آرگومان یا یک آرایه را به عنوان ورودی گرفته و بر روی آن محاسبات خود را انجام داده و خروجی را نشان دهند:
Math.max [14, 35, -7, 46, 98]... # 98 Math.min [14, 35, -7, 46, 98]... # -7
Math.max.apply(Math, [14, 35, -7, 46, 98]); Math.min.apply(Math, [14, 35, -7, 46, 98]);
الگوهای طراحی، سندها و راه حلهای از پیش تعریف شده و تست شدهای برای مسائل و مشکلات روزمرهی برنامه نویسی میباشند که هر روزه ما را درگیر خودشان میکنند. هر چقدر مقیاس پروژه وسیعتر و تعداد کلاسها و اشیاء بزرگتر باشند، درگیری برنامه نویس و چالش برای مرتب سازی و خوانایی برنامه و همچنین بالا بردن کارآیی و امنیت افزونتر میشود. از همین رو استفاده از ساختارهایی تست شده برای سناریوهای یکسان، امری واجب تلقی میشود.
الگوهای طراحی از لحاظ سناریو، به سه گروه عمده تقسیم میشوند:
1- تکوینی: هر چقدر تعداد کلاسها در یک پروژه زیاد شود، به مراتب تعداد اشیاء ساخته شده از آن نیز افزوده شده و پیچیدگی و درگیری نیز افزایش مییابد. راه حلهایی از این دست، تمرکز بر روی مرکزیت دادن به کلاسها با استفاده از رابطها و کپسوله نمودن (پنهان سازی) اشیاء دارد.
2- ساختاری: گاهی در پروژهها پیش میآید که میخواهیم ارتباط بین دو کلاس را تغییر دهیم. از این رو امکان از هم پاشی اجزایِ دیگر پروژه پیش میآید. راه حلهای ساختاری، سعی در حفظ انسجام پروژه در برابر این دست از تغییرات را دارند.
3- رفتاری: گاهی بنا به مصلحت و نیاز مشتری، رفتار یک کلاس میبایستی تغییر نماید. مثلا چنانچه کلاسی برای ارائه صورتحساب داریم و در آن میزان مالیات 30% لحاظ شده است، حال این درصد باید به عددی دیگر تغییر کند و یا پایگاه داده به جای مشاهدهی تعدادِ معدودی گره از درخت، حال میبایست تمام گرهها را ارائه نماید.
الگوی فکتوری:
الگوی فکتوری در دستهء اول قرار میگیرد. من در اینجا به نمونهای از مشکلاتی که این الگو حل مینماید، اشاره میکنم:
فرض کنید یک شرکت بزرگ قصد دارد تا جزییات کامل خرید هر مشتری را با زدن دکمه چاپ ارسال نماید. چنین شرکت بزرگی بر اساس سیاستهای داخلی، بر حسب میزان خرید، مشتریان را به چند گروه مشتری معمولی و مشتری ممتاز تقسیم مینماید. در نتیجه نمایش جزییات برای آنها با احتساب میزان تخفیف و به عنوان مثال تعداد فیلدهایی که برای آنها در نظر گرفته شده است، تفاوت دارد. بنابراین برای هر نوع مشتری یک کلاس وجود دارد.
یک راه این است که با کلیک روی دکمهی چاپ، نوع مشتری تشخیص داده شود و
به ازای نوع مشتری، یک شیء از کلاس مشخص شده برای همان نوع ساخته شود.
// Get Customer Type from Customer click on Print Button int customerType = 0; // Create Object without instantiation object obj; //Instantiate obj according to customer Type if (customerType == 1) { obj = new Customer1(); } else if (customerType == 2) { obj = new Customer2(); } // Problem: // 1: Scattered New Keywords // 2: Client side is aware of Customer Type
همانگونه که مشاهده مینمایید در این سبک کدنویسی غیرحرفهای، مشکلاتی مشهود است که قابل اغماض نیستند. در ابتدا سمت کلاینت دسترسی مستقیم به کلاسها دارد و همانگونه که در شکل بالا قابل مشاهده است کلاینت مستقیما به کلاس وصل است. مشکل دوم عدم پنهان سازی کلاس از دید مشتری است.
راه حل: این مشکل با استفاده از الگوی فکتوری قابل حل است. با استناد به الگوی فکتوری، کلاینت تنها به کلاس فکتوری و یک اینترفیس دسترسی دارد و کلاسهای فکتوری و اینترفیس، حق دسترسی به کلاسهای اصلی برنامه را دارند.
گام نخست: در ابتدا یک class library به نام Interface ساخته و در آن یک کلاس با نام ICustomer می سازیم که متد Report() را معرفی مینماید.
//Interface
namespace Interface { public interface ICustomer { void Report(); } }
گام دوم: یک class library به نام MainClass ساخته و با Add Reference کلاس Interface را اضافه نموده، در آن دو کلاس با نام Customer1, Customer2 میسازیم و using Interface را Import مینماییم. هر دو کلاس از ICustomer ارث میبرند و سپس متد Report() را در هر دو کلاس Implement مینماییم.
// Customer1 using System; using Interface; namespace MainClass { public class Customer1 : ICustomer { public void Report() { Console.WriteLine("این گزارش مخصوص مشتری نوع اول است"); } } } //Customer2 using System; using Interface; namespace MainClass { public class Customer2 : ICustomer { public void Report() { Console.WriteLine("این گزارش مخصوص مشتری نوع دوم است"); } } }
گام سوم: یک class library به نام FactoryClass ساخته و با Add Reference کلاس Interface, MainClass را اضافه نموده، در آن یک کلاس با نام clsFactory می سازیم و using Interface, using MainClass را Import مینماییم. پس از آن یک متد با نام getCustomerType ساخته که ورودی آن نوع مشتری از نوع int است و خروجی آن از نوع Interface-ICustomer و بر اساس کد نوع مشتری object را از کلاس Customer1 و یا Customer2 میسازیم و آن را return می نماییم.
//Factory using System; using Interface; using MainClass; namespace FactoryClass { public class clsFactory { static public ICustomer getCustomerType(int intCustomerType) { ICustomer objCust; if (intCustomerType == 1) { objCust = new Customer1(); } else if (intCustomerType == 2) { objCust = new Customer2(); } else { return null; } return objCust; } } }
گام چهارم (آخر): در قسمت UI Client، کد نوع مشتری را از کاربر دریافت کرده و با Add Reference کلاس Interface, FactoryClass را اضافه نموده (دقت نمایید هیچ دسترسی به کلاسهای اصلی وجود ندارد)، و using Interface, using FactoryClass را Import مینماییم. از clsFactory تابع getCustomerType را فراخوانی نموده (به آن کد نوع مشتری را پاس میدهیم) و خروجی آن را که از نوع اینترفیس است به یک object از نوع ICustomer نسبت میدهیم. سپس از این object متد Report را فراخوانی مینماییم. همانطور که از شکل و کدها مشخص است، هیچ رابطه ای بین UI(Client) و کلاسهای اصلی برقرار نیست.
//UI (Client) using System; using FactoryClass; using Interface; namespace DesignPattern { class Program { static void Main(string[] args) { int intCustomerType = 0; ICustomer objCust; Console.WriteLine("نوع مشتری را وارد نمایید"); intCustomerType = Convert.ToInt16(Console.ReadLine()); objCust = clsFactory.getCustomerType(intCustomerType); objCust.Report(); Console.ReadLine(); } } }
ویژگیهای C# 11.0
Mocking چیست؟
فرض کنید برنامهای را داریم که از تعدادی کلاس تشکیل شدهاست. در این بین میخواهیم تعدادی از آنها را به صورت ایزولهی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاسها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده میکنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون میخواهیم این کلاس را به صورت ایزولهی از کل سیستم آزمایش کنیم، اینبار بجای استفادهی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونهی تقلیدی یا Mock object در اینجا، جایگزین میکنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار میگیرد، با نمونهی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمونهای واحد است.
دلایل و مزایای استفادهی از Mocking
- یکی از مهمترین دلایل استفادهی از Mocking، کاهش پیچیدگی تنظیمات اولیهی نوشتن آزمونهای واحد است. برای مثال اگر در برنامهی خود از تزریق وابستگیها استفاده میکنید و کلاسی دارای چندین وابستگی تزریق شدهی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگیها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگیهای تزریق شدهی در یک کلاس استفاده نکند. در این حالت، Mocking میتواند تنظیمات پیچیدهی وهله سازی این کلاس را به حداقل برساند.
- Mocking میتواند سبب افزایش سرعت اجرای آزمونهای واحد نیز شود. برای مثال با تقلید سرویسهای خارجی مورد استفادهی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، میتوان میزان I/O و همچنین زمان صرف شدهی به آنرا به حداقل رساند.
- از mock objects میتوان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده میکنید یا اعداد اتفاقی و امثال آن، هربار که آزمونهای واحد را اجرا میکنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمونهای نوشته شده با مشکل مواجه میشوند. به کمک mocking میتوان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی میتوان mock objects را تهیه کرد، میتوان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویسهای مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که میتواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، میتوان توسط mocking به یک نمونهی قابل پیش بینی و پایدار مخصوص آزمونهای برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینهای را شارژ میکند، میتوان توسط mocking، هزینهی آزمونهای برنامه را کاهش داد.
Unit test چیست؟
بدیهی است در کنار آزمایش ایزولهی قسمتهای مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمونها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره میگویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح میشود که یک Unit چیست؟
در برنامهای که از چندین کلاس تشکیل میشود، به یک کلاس، یک Unit گفته میشود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار میکنند (کلاسی که از چندین وابستگی استفاده میکند)، اینها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوهی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.
واژههای متناظر با Mock objects
در حین مطالعهی منابع مرتبط با آزمونهای واحد ممکن است با این واژههای تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آنها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژهاست. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیهی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده میشوند. این پارامترها، هیچگاه در آزمایشهای انجام شده مورد استفاده قرار نمیگیرند.
- از Stubs برای ارائهی پاسخهایی مشخص به فراخوانها استفاده میشود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده میشود. آیا متدی یا خاصیتی مورد استفاده قرار گرفتهاست یا خیر؟
باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانهی Moq تهیه میکنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره میشود، هر سه مفهوم مدنظر هستند.
واژهی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks میباشد. در کل هر زمانیکه یک شیء مورد استفادهی در زمان اجرای برنامه را جهت آزمایش سادهتر آن جایگزین میکنید، یک Test double را ایجاد کردهاید.
بررسی ساختار برنامهای که میخواهیم آنرا آزمایش کنیم
در این سری قصد داریم یک برنامهی وام دهی را آزمایش کنیم که قسمتهای مختلف آن دارای وابستگیهای خاصی میباشند. ساختار این برنامه را در ادامه مشاهده میکنید:
موجودیتهای برنامهی وام دهی
namespace Loans.Entities { public class Applicant { public int Id { set; get; } public string Name { set; get; } public int Age { set; get; } public string Address { set; get; } public decimal Salary { set; get; } } }
namespace Loans.Entities { public class LoanProduct { public int Id { set; get; } public string ProductName { set; get; } public decimal InterestRate { set; get; } } }
namespace Loans.Entities { public class LoanApplication { public int Id { set; get; } public LoanProduct Product { set; get; } public LoanAmount Amount { set; get; } public Applicant Applicant { set; get; } public bool IsAccepted { set; get; } } public class LoanAmount { public string CurrencyCode { get; set; } public decimal Principal { get; set; } } }
مدلهای برنامهی وام دهی
namespace Loans.Models { public class IdentityVerificationStatus { public bool Passed { get; set; } } }
سرویسهای برنامهی وام دهی
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Initialize(); bool Validate(string applicantName, int applicantAge, string applicantAddress); void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid); void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status); } }
namespace Loans.Services.Contracts { public interface ICreditScorer { int Score { get; } void CalculateScore(string applicantName, string applicantAddress); } }
using System; using Loans.Entities; using Loans.Services.Contracts; namespace Loans.Services { public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0; private const int MinimumAge = 18; private const int MinimumCreditScore = 100_000; private readonly IIdentityVerifier _identityVerifier; private readonly ICreditScorer _creditScorer; public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); } public bool Process(LoanApplication application) { application.IsAccepted = false; if (application.Applicant.Salary < MinimumSalary) { return application.IsAccepted; } if (application.Applicant.Age < MinimumAge) { return application.IsAccepted; } _identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address); if (!isValidIdentity) { return application.IsAccepted; } _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); if (_creditScorer.Score < MinimumCreditScore) { return application.IsAccepted; } application.IsAccepted = true; return application.IsAccepted; } } }
using System; using Loans.Models; using Loans.Services.Contracts; namespace Loans.Services { public class IdentityVerifierServiceGateway : IIdentityVerifier { public DateTime LastCheckTime { get; private set; } public void Initialize() { // Initialize connection to external service } public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = DateTime.Now; Disconnect(); return isValidIdentity; } private void Connect() { // Open connection to external service } private bool CallService(string applicantName, int applicantAge, string applicantAddress) { // Make call to external service, interpret the response, and return result return false; // Simulate result for demo purposes } private void Disconnect() { // Close connection to external service } public void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid) { throw new NotImplementedException(); } public void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status) { throw new NotImplementedException(); } } }
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را دادهاست.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شدهی به آن نیز میباشد:
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
تمام این منطق نیز در متد Process آن قابل مشاهدهاست که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.
نوشتن اولین تست، برای برنامهی وام دهی
در اولین تصویر این قسمت، پروژهی class library دومی را نیز به نام Loans.Tests مشاهده میکنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Loans\Loans.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" /> <PackageReference Include="MSTest.TestFramework" Version="2.0.0" /> </ItemGroup> </Project>
اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا میکند:
using Loans.Entities; using Loans.Services; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var processor = new LoanApplicationProcessor(null, null); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و میخواهیم بررسی کنیم که آیا LoanApplicationProcessor میتواند آنرا بر اساس مقدار MinimumSalary، رد کند یا خیر؟
public class LoanApplicationProcessor { private const decimal MinimumSalary = 1_500_000_0;
در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شدهاند؛ چون میدانیم که بررسی MinimumSalary پیش از سایر بررسیها صورت میگیرد و اساسا در این آزمایش، نیازی به این وابستگیها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه میشویم:
Test method Loans.Tests.LoanApplicationProcessorShould.DeclineLowSalary threw exception: System.ArgumentNullException: Value cannot be null. Parameter name: identityVerifier
public LoanApplicationProcessor( IIdentityVerifier identityVerifier, ICreditScorer creditScorer) { _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier)); _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer)); }
نصب کتابخانهی Moq جهت برآورده کردن وابستگیهای کلاس LoanApplicationProcessor
در این آزمایش چون وجود وابستگیهای در سازندهی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آنها نیز الزامی است، میخواهیم توسط کتابخانهی Moq، دو نمونهی تقلیدی از آنها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفتهی در سازندهی کلاس LoanApplicationProcessor، آنها را ارائه کنیم.
کتابخانهی بسیار معروف Moq، با پروژههای مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، میتوان یکی از دو دستور زیر را صادر کرد:
> dotnet add package Moq > Install-Package Moq
اما چرا کتابخانهی Moq؟
کتابخانهی Moq این اهداف را دنبال میکند: سادهاست، به شدت کاربردیاست و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بستهی نیوگت آن میلیونی است.
پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح میکنیم:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void DeclineLowSalary() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsFalse(application.IsAccepted); } } }
کار با ذکر new Mock شروع شده و آرگومان جنریک آنرا از نوع وابستگیهایی که نیاز داریم، مقدار دهی میکنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر میکند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیدهاست:
گاهی از اوقات جایگزین کردن یک وابستگی null با نمونهی Mock آن کافی نیست
در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
using Loans.Entities; using Loans.Services; using Loans.Services.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void Accept() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsTrue(application.IsAccepted); } } }
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفادهی از دو mock object به عنوان وابستگیهای مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آنها انجام ندادهایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمیدهند. برای مثال مرحلهی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
_identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-01.zip