تقلیدکردن یا Mocking در فرآیند تست نویسی لاراول
همانطور که تا به الان با امکانات و ابزارهای PHPUnit بعنوان ابزار تست پیشفرض در لاراول آشنا شدیم دیدیم که قابلیتهایی که تیم توسعه لاراول برای پیادهسازی با روش TDD در اختیار ما قرار میده بسیار در روند کاربردیه و اگر درسا پیاده بشه میتونه سرعت توسعه اپلیکیشنهای مارو بیشتر بکنه! در این دوره شما با نحوه پیادهسازی یک پروژه ساده مدیریت وظایف همراه با فرایند احراز هویت Sanctum روی یک بستر API آشنا شدید. اما ابزارهای تست به همینجا ختم نمیشن و قابلیتهای دیگری هم وجود داره!
یکی از این قابلیتها ابزار mocking در روند تست نویسی هست که کمک میکنه بخشی از فرآیندها که نمیخوایم در روند تست وارد بشن یه جورایی تقلید یا شبیهسازی بشن (اداشونو درمیاره!)
از این جهت زمانی که شما مثلا در کنترلرتون یک ایمیل ارسال میکنید یا فایلی دانلود میشه یا مواردی از این دست واقعا نمیخواهید این کارها در روند تست انجام بشه! بنابراین این عملیات رو شبیهسازی میکنیم اصطلاحا mock میکنید!
در ادامه و در این مقاله در مورد نحوه mock کردن یکسری فرآیندهای خاص در خلال تست کردن و فرآیند توسعه به روش TDD آشنا خواهید شد.
همانطور که در تصویر زیر هم مشاهده میکنید قابلیت mocking در لاراول موارد زیر رو شامل میشه. شما میتونید یک job، رخداد، ارسال ایمیل یک Facade و حتی کار با فایلها هم mock کنید:


در این مقاله میخواهیم برای وظایفمون قابلیت ددلاین رو اضافه کنیم و با کمک یک job از طریق ایمیل به کاربر زمان پایان اون وظیفه رو یادآوری کنیم.
خب برای شروع در ادامه پروژهای که در این دوره روی آن کار میکنیم یک دستور migration جدید برای افزودن ستون ended_at
به جدول وظایفمون مانند زیر مینویسیم:
class AddEndedAtToTasksTable extends Migration
{
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->timestamp('ended_at')->nullable();
});
}
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('ended_at');
});
}
}
حالا دستور migrate رو اجرا میکنیم. بعد از مایگریت من برای اطمینان یکبار تست رو اجرا میکنم تا مطمئن بشم در حال حاضر همچی درسته:


حالا تست جدید رو در کلاس TaskTest مینویسیم تا ببینیم آیا کاربر میتونه یک وظیفه همراه با تاریخ اتمام ذخیره کنه یا نه!


در اینجا در ابتدا یک کاربر با factory که ایجاد و با استفاده از ابزار sanctum با همان کاربر یک وظیفه رو ذخیره میکنم. در مورد احراز هویت و ارسال درخواستهای معتبر در مقالات قبلی مطالبی گفته شده که درصورت نیاز میتونید مطالعه کنید:
در نهایت هم برای اطمینان از اینکه تاریخ پایان وظیفه که وارد کردیم درست وارد شده یا نه با متد assertNotNull ارزیابی میکنیم:


درسته! همانطور که انتظار میره تاریخ اتمام وظیفه ذخیره نشده، علت هم تعریف نکردن rule مربوط به اعتبارسنجی تاریخ اتمام هنگام ذخیره وظیفه جدید هست، برای این منظور وارد Http/Requests/TaskRequest.php
میشیم و بصورت زیر اون رو ویرایش میکنیم:


بار دیگه تست رو اجرا میکنیم که اینبار تست موفقیت آمیز بود!
حالا من میخوام از فرمت درست تاریخ وارد شده اطمینان پیدا کنم. برای منظور تست دیگری رو مانند زیر مینویسیم:


همانطور که مشاهده میکنید توی این تست میخواهیم که با دیتای اشتباه تاریخ اتمام وظیفه خطای فرمت دریافت کنیم یعنی در واقع تست زمانی پاس میشه که خطای فرمت دریافت بکنه! اما با اجرای تست اینطور نشد. من از تابع dd استفاده کردم که همانطور که مشاهده میکنید اطلاعات ورودی درست نیست:


برای این منظور هم باید از rule مربوطه و استفاده کنید برای آشنایی با اعتبارسنجی میتوانید مقالهای که در این زمینه نوشتیم رو مطالعه کنید:
نهایتا مجددا rule رو بصورت زیر ویرایش کردیم که نتیجه برای کلاس TaskRequest
بصورت زیر شد:
public function rules()
{
return [
'title' => 'required',
'description' => 'sometimes',
'ended_at' => 'sometimes|date'
];
}
حالا برای نیازمندی بعدی در پروژه نمیخواهیم کاربر زمان وارد شده رو پیش از زمان فعلی وارد کنه. یعنی نمیخواهیم کاربر بتونه تاریخ اتمام یک وظیفه رو در گذشته وارد کنه!


و مجددا تست رو اجرا میکنیم که همانطور که انتظار میره خطا دریافت میکنیم که دلیلش همون ruleهاست پس بار دیگه به کلاس TaskRequest
برمیگردیم و بصورت زیر ویرایش میکنیم که نتیجه تستهامون موفقیت آمیز میکنه!
public function rules()
{
return [
'title' => 'required',
'description' => 'sometimes',
'ended_at' => 'sometimes|date|after:now'
];
}
در گام بعدی میخواهیم از فرمت تاریخی که وارد کردیم اطمینان پیدا کنیم پس برای این منظور تست زیر رو مینویسیم:


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


برای مشاهده نوع این متغیر کد زیر رو داخل تست قرار دادم همانطور که مشاهده خواهید کرد فرمت تاریخ مورد نظر ما از نوع رشته هست.
dd($task->ended_at . ' is a ' . gettype($task->ended_at));
// output: "2021-12-30 17:19:08 is a string"
برای رفع این مورد هم باید به مدل Task برگردیم و نوع این متغیر رو در کلاس بعنوان تاریخ تعریف کنیم این کار معمولا برای ویژگی زمانی غیر از updated_at و created_at باید انجام بشه:


حالا اگر بار دیگه تست رو اجرا کنیم خواهید دید که ارزیابی ما برای نوع متغیر زمانی ended_at موفق خواهد بود. من همین تستهارو برای بروزرسانی یک وظیفه هم نوشتم که نهایت با تغییراتی مشابه این تستها هم پاس خواهند شد.
ارزیابی Job
خب بیایید در ادامه یک job ایجاد کنیم:
php artisan make:job TaskJob
در مورد نحوه اجرای job و crontab برای مدیریت اجرای آنها در مقاله دیگری نوشتم که میتونید برای اطلاعات بیشتر به اون مراجعه کنید:
اما درون TaskJob من میخوام که در میان کاربران سیستم وظایفی که در حال حاضر تاریخ آنها گذشته ایمیل ارسال شود و پس از اون تاریخ پایان وظیفه مورد نظر رو null میکنیم، در اینصورت دیگه برای اون وظیفه ایمیل ارسال نمیشه البته بهتر مییود ستونی برای این مورد در نظر میگرفتم (روشی که در اینجا پیاده کردیم قطعا میشه بهتر نوشت اما اینجا فقط جنبه آموزشی داره پس بذارید همه چیز رو ساده در نظر بگیریم).


خب حالا یک کلاس Test جدید ایجاد میکنیم:
php artisan make:test JobTest
در تست جدید ابتدا یک کاربر ایجاد و با آن یک وظیفه با تاریخ انقضای یک ثانیه بعد ذخیره میکنیم. حتما بخاطر دارید که امکان ذخیره وظیفه با تاریخ در گذشته وجود نداره پس برای اینکه بخواهیم در Job فرايند ارسال ایمیل اجرا بشه زمان رو خیلی نزدیک در نظر میگیریم یعنی یک ثانیه بعد از زمان حالا!


این تست قطعا پاس میشه چرا که قبلا در تست دیگری بررسی کردیم اما اجازه بدید در انتهای ایجاد وظیفه TaskJob رو اجرا یا اصطلاحا dispatch کنیم:


همانطور که مشاهده میکنید من یک وقفه ۲ ثانیهای ایجاد کردم تا تاریخ اتمام وظیفه به اتمام برسه و نهایتا بعد از اجرای Job وظیفه مورد نظر منقضی میشه که در اینصورت انتظار داریم تعداد این وظایف برابر با یک باشد پس از اجرای این تست ارزیابی ما با موفقیت همراه بود ولی یک موردی بوجود میاد و اونم ارسال ایمیل در جریان تست هست.
معمولا در روند تست ما نمیخواهیم برخی از عملیات مانند ارسال ایمیل اجرا شوند. در اینجا هم نمیخواهیم ایمیلی ارسال بشه و همانطور که گفته شد در نظر داریم فرایند ارسال ایمیل تقلید یا همون mock بشه!
برای این منظور کافیه متد fake رو از کلاس Mail فراخوانی کنیم با این دستور تمامی عملیاتی که ارسال ایمیل دارند mock میشن!


همانطور که مشاهده میکنید من در اینجا پیش از dispatch کردن TaskJob دستور ()Mail:fake
رو اجرا کردم و نتیجه این شد که ارسال ایمیل که قبل در فرایند تست داشتیم اینبار ارسال نشد. ضمنا دستورهای دیگری وجود دارند که ميتونیم با کمک اونها از اجرای ارسال فیک ایمیل اطمینان پیدا میکنیم:


همانطور که در ابتدا گفتیم عملیات moking و تقلید کردن در لاراول موارد زیادی رو شامل میشه و ما میتونیم هرجایی که لازم داشتیم برای تمرکز روی قسمتی که لازم به ارزیابی و تست داره این عملیات رو پیاده کنیم. ما در اینجا هدفمون خود Job بود و میخواستیم مشاهده کنیم که آیا بدرستی فرآیند یادآوری اتمام وظایف اتفاق میافته یا نه!
بیایید تست دیگری رو بنویسیم ولی اینبار خود TaskJob رو تقلید کنیم. قاعدتا اینبار انتظار داریم تعداد وظایفی که تاریخ اتمام آنها گذشته و null شدند برابر با صفر باشه:


با اجرای این تست هم خواهیم دید که عملیات ارزیابی با موفقیت انجام شده!
در قطعه کد بالا من برای mock کردن TaskJob از کلاس Bus استفاده کردم البته این تستی نیست که ما نیاز داشته باشیم ولی همانطور که مشاهده کردید عملیات اجرای جاب بصورت فیک انجام شده. بصورت مشابه شما میتوانید Event ،Mail ،Notification ،Queue و Storage رو در عملیات تست شبیهسازی یا تقلید یا همون mock کنید!
در این آموزش با نحوه شبیهسازی عملیات ارسال ایمیل در جریان تست کردن و بطور کلی عملیات mocking آشنا شدید که روند ارزیابی فرآیندها رو آسونتر میکنه و اجازه میده دقیقا همون قسمتی که میخواهید رو در روند توسعه ارزیابی کنید.
امیدوارم از این آموزش هم لذت بره باشید!