چطور با روش توسعه TDD یک معماری RESTful API در لاراول پیاده‌سازی کنیم

در مقاله قبلی مفاهیم و اهمیت روش توسعه TDD و نحوه شروع بکار با PHPUnit در لاراول رو توضیح دادم در ادامه و در این مقاله ابتدا نیازمندی‌های یک سیستم رو بررسی می‌کنیم و به شیوه قدم به قدم و ساده با روش توسعه TDD،  تست‌های مورد نیاز برای معماری  RESTful API رو بدون نیاز به Postman یا حتی مرورگر پیاده‌سازی می‌کنیم.


خب اجازه بدید در ابتدا پروژه‌ای که در این دوره می‌خواهیم با روش توسعه TDD روی اون کار کنیم رو تعریف کنیم.

تصور کنید از شما خواسته شده که به عنوان بک‌اند کار کلیه فرآیندهای مربوط به یک اپلیکیشن مدیریت وظایف یا همون to-do list رو برای موبایل توسعه بدید، که در اون هر کاربر می‌تونه یک سری وظایف رو ایجاد و مدیریت کنه، و حالا می‌خواهید با استفاده از روش توسعه TDD فرایند‌های مربوط به این اپ رو راه اندازی کنید. در مقاله قبلی در مورد نحوه راه‌اندازی و تعریف کلاس تست در لاراولی نکاتی رو گفتیم. چنانچه با این ابزار آشنایی ندارید پیشنهاد می‌کنم پیش از ادامه مقاله حتما از آدرس زیر آن را مطالعه کنید:

بیایید برای شروع یک کلاس تست جدید برای وظایفمون تعریف کنیم:

php artisan make:test TaskTest

سپس یک تابع جدید بصورت زیر در نظر بگیرید:

Image for post
Image for post

همانطور که در مقاله قبل هم اشاره کردم درصورتیکه شما مستقیما با phpunit در تست‌هاتون استفاده می‌کنید باید یک annotation مشابه بالا قبل از تابع مورد نظرتون بنویسید تا بتونید تابع مورد نظر رو تست کنید. خب همانطور که در تصویر زیر مشاهده می‌کنید بدلیل اینکه در تابع add_new_task هیچ بررسی انجام ندادیم نتیجه ارزیابی  درواقع داره می‌گه چیزی برای تست وجود نداره!

Image for post
Image for post

البته بهتره یک alias بصورت زیر تعریف کنید تا دسترسی سریع‌تری برای اجرای تست‌هاتون داشته باشید:

// in ~/.bashrc
alias pf='clear && phpunit --filter';

حالا کافیه برای اجرای تست‌هامون روی هر تابع بصورت زیر عمل کنید:

pf add_new_task

تست ایجاد رکورد جدید

برای تست کردن عملیات ارسال دیتا کلاس‌های Test قابلیت‌های بسیار زیاد و جالبی دارند، برای نمونه برای ارسال درخواست به سرور از نوع get یا post کافیه بصورت زیر خیلی ساده روی مسیر‌هایی که می‌خواهید اینکار رو انجام بدید:

$response = $this->get('<uri>');

یکی دیگه از قابلیت‌های مهم کلاس Test توابع assert هستند، همانطور که در تصویر زیر مشاهده می‌کنید برای یک درخواست ساده تعداد زیادی از این توابع کمکی وجود داره که شما می‌تونید با کمک اونها هر وضعیتی رو که می‌خواهید بررسی کنید

Image for post
Image for post

در ادامه ما میخواهیم یک درخواست ارسال وظیفه جدید و ایجاد یک رکورد رو تست کنیم. فرض کنید فعلا وظایف ما تنها یک عنوان و یک توضیح داشته باشند برای این منظور من یک درخواست post با یکسری دیتا ایجاد می‌کنم و از تابع assertOk برای اطمینان از اینکه درخواستم با موفقیت انجام شده و سرور پاسخ با کد ۲۰۰ برمی‌گردونه، استفاده می‌کنم (البته می‌تونید اینجا از assertStatus(200)‍ هم استفاده کنید):

Image for post
Image for post

پس از اجرای تست خواهیم داشت:

Image for post
Image for post

خب همانطور که مشخصه ما مسیری برای این درخواست تعریف نکردیم (در اینجا من روی یک پروژه خام کار می‌کنم و هنوز هیچ تغییری رو اعمال نکردم). اول یک کنترلر می‌سازم و بعد به routes/api.php میرم و آدرس‌دهی رو انجام می‌دم و بار دیگه تست رو اجرا می‌کنم:

Image for post
Image for post

اینبار خطای ۵۰۰ رو گرفتیم! 

ضمنا توجه کنید اگه بخواهیم متوجه جزییات خطاها و عوامل ایجاد اونها بشیم. بهتره یک دستور دیگری رو در تستمون وارد کنیم تا از فرآیند مدیریت Exception ها که لاراول با middlewareها روی درخواستهای انجام میده فعلا خلاص بشیم و درک بهتری به خطاها داشته باشیم. خب برای این منظور بصورت زیر عمل می‌کنیم:

Image for post
Image for post

و حالا مجددا تست رو اجرا می‌کنیم:

Image for post
Image for post

اینبار خطا مشخصا داره می‌گه تابع store تعریف نشده پس به کنترلر برمی‌گردیم و دستور ذخیره کردن وظیفه جدید رو بصورت زیر تعریف می‌کنیم (توجه داشته باشید در اینجا هدف آموزش هست برای همین مراحل به ساده‌تری شکل ممکن انجام میشن! ):

و حالا خطای جدید :

Image for post
Image for post

مشاهده می‌کنید که خیلی تست کردن و بررسی علل خطا در این روش راحت‌تره، اینجا داره توضیح میده که کلاس Task داخل کنترلر وجود نداره! پس بریم که مدل Task رو ایجاد کنیم:

php artisan make:model Task

به کنترلر برمی‌گردیم و کلاس‌ Task رو وارد می‌کنیم و نهایتا بار دیگه تست رو اجرا می‌کنیم:

Image for post
Image for post

حالا همانطور که مشاهده می‌کنید خطای mass assignment رو داریم در این باره در مقاله زیر صحبت شد که توصیه می‌کنم اگر آشنایی ندارید حتما مطالعه کنید:

 پس از برطرف کردن این مورد مجددا تست رو اجرا می‌کنیم:

Image for post
Image for post

همانطور که مشاهده می‌کنید خطای مربوط به دیتابیس هست و به دلیل نبود دسترسی به جدول مدل Task بوجود اومده اما همانطور که گفتیم در عملیات تست ما یک دیتابیس دیگری رو در نظر گرفتیم، اما چطور و کجا باید migrate انجام بدیم تا جدول tasks رو ایجاد کرد؟

برای این منظور یک خاصیت در کلاس‌های تست تعریف شده که هربار که تست رو اجرا می‌کنید جداول رو در حافظه ایجاد خواهد کرد. پس به یاد داشته باشید هر بار که تست اجرا میشه دیتابیس ما fresh شده‌. حالا به کلاس تست Task برمی‌گردیم و بصورت زیر اون رو ویرایش می‌کنیم:

Image for post
Image for post

حالا پس از ایجاد migration و تعریف ویژگی‌هامون بصورت زیر یکبار دیگه دستور اجرای تست رو صدا میزنیم:

public function up()
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->string('description');
        $table->timestamps();
    });
}
Image for post
Image for post

خب این نشون میده که جدول بدرستی ایجاد شده و ما تونستیم یک رکورد جدید task رو وارد کنیم.

اما یکی از مهم‌ترین مواردی که در ارسال دیتا به سرور باید انجام بشه اعتبارسنجی اطلاعات هست. برای این منظور تابع دیگری رو مانند زیر ایجاد می‌کنیم :

Image for post
Image for post

همانطور که مشاهده می‌کنید در اینجا من قابلیت ‍withoutExceptionHandling رو برداشتم و از یک assertion جدید دیگری استفاده کردم، چرا که در اینجا می‌خوایم اگر خطایی در response وجود داشته باشه تست موفق اعلام بشه. حالا یکبار دیگه اگر تست رو اجرا کنیم خواهیم داشت:

Image for post
Image for post

این خطا نشون میده خطای validation برای اطلاعات اشتباهی که ما ارسال کردیم به درستی کار نمیکنه می‌تونید برای اطمینان قابلیت ‍withoutExceptionHandling رو برگردونید و خطای دیتابیس رو بصورت زیر ببینید:

Image for post
Image for post

اما خطای که دراینجا داریم بدلیل nullable نبودن ستون‌ها در جدول و پاس دادن مقدار null به اونها هست که باید هندل بشه بیایید برگردیم و درون کنترلر یک اعتبارسنجی ایجاد کنیم:

Image for post
Image for post

خب همانطور که مشاهده می‌کنید من در اعتبارسنجی توضیحات رو nullable کردم برای اعمال این تغییر وارد migration بشید و این ویژگی رو به ستون توضیحات اضافه کنید:

$table->string('description')->nullable();

حالا اگر بار دیگه تست رو اجرا کنید خواهید دید که اینبار خطایی درکار نیست و اطلاعات اشتباهی که ارسال شده به درستی هندل شدند. این یعنی متد ‍assertSessionHasErrors داره به ما میگه که داخل session خطایی مربوط به دیتایی که ارسال کردیم وجود داره، از این جهت تست ما با موفقیت همراه شد!

حالا بیایید یک رکورد رو بروزرسانی کنیم برای اینکار از اونجایی که دیتابیس در هر اجرا fresh میشه پس ابتدا یک رکورد رو ایجاد و نهایتا اون رو بروزرسانی می‌کنیم. 

Image for post
Image for post

با اجرای تست خواهید دید که بررسی برابری برای مقادیر رکورد پس از بروزرسانی با شکست همراه بوده این یعنی بروزرسانی بدرستی اتفاق نیافتاده است:

Image for post
Image for post

دلیل این خطا هم واضحه چرا که مسیر و تابع برای بروزرسانی تعریف نشدند. اگر متد ‍withoutExceptionHandling رو هم اضافه کنید مشخصا همین خطا رو خواهید دید.

در ادامه من این موارد رو برطرف کردم و بار دیگه تست رو اجرا کردم که بدون هیچ مشکلی عملیات بروز رسانی هم انجام شد. بیایید حالا یکبار کل کلاس‌ رو بررسی کنیم و تمامی تست‌هایی که نوشتیم رو اجرا کنید خواهید دید که تمامی تست پاس شدند و بدون مشکل عملیات ایجاد و بروزرسانی رکورد و اعتبارسنجی اطلاعات ارسالی بررسی شده است:

Image for post
Image for post

به همین شیوه من قابلیت حذف رکورد رو اضافه و تابع تست این عملیات هم همانند زیر تعریف کردم:

Image for post
Image for post

تابع assertCount همانطور که مشاهده می‌کنید برای مقایسه تعداد یک آرایه با یک مقدار پیش فرض تعریف شده. ما در اینجا می‌خواهیم مطمئین بشیم پس از ایجاد و حذف یک رکورد تعداد رکوردها هنوز صفره که یعنی حذف کردن با موفقیت انجام شده!


همانطور که مشاهده می‌کنید با این روش می‌تونید بدون درگیر شدن با قسمت‌های دیگه و بسادگی سرویس‌ خودتون رو پیاده‌سازی کنید. یکی دیگه از مزیت‌های این روش در عملیات توسعه هست زمانی که شما ویژگی دیگری به سیستم اضافه می‌کنید و می‌خواهید ببینید تغییرات شما در ارتباط با کل سیستم به درستی کار میکنه یا نه! برای اینکار تنها کافیه که تست مربوطه رو اجرا کنید و مشاهده کنید عملیات مورد نظر بدرستی پاس میشه یا نه!!!

 

امیدوارم از این آموزش لذت برده باشید.