تقلیدکردن یا Mocking در فرآیند تست نویسی لاراول

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

یکی از این قابلیت‌ها ابزار mocking در روند تست نویسی هست که کمک می‌کنه بخشی از فرآیندها که نمی‌خوایم در روند تست وارد بشن یه جورایی تقلید یا شبیه‌سازی بشن (اداشونو درمیاره!)

از این جهت زمانی که شما مثلا در کنترلرتون یک ایمیل ارسال می‌کنید یا فایلی دانلود میشه یا مواردی از این دست واقعا نمی‌خواهید این کارها در روند تست انجام بشه! بنابراین این عملیات رو شبیه‌سازی می‌کنیم اصطلاحا mock می‌کنید!

در ادامه و در این مقاله در مورد نحوه mock کردن یکسری فرآیند‌های خاص در خلال تست کردن و فرآیند توسعه به روش TDD آشنا خواهید شد.


همانطور که در تصویر زیر هم مشاهده می‌کنید قابلیت mocking در لاراول موارد زیر رو شامل میشه. شما می‌تونید یک job، رخداد، ارسال ایمیل یک Facade و حتی کار با فایل‌ها هم mock کنید:

Image for post
Image for post

در این مقاله می‌خواهیم برای وظایفمون قابلیت ددلاین رو اضافه کنیم و با کمک یک 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 رو اجرا می‌کنیم. بعد از مایگریت من برای اطمینان یکبار تست رو اجرا می‌کنم تا مطمئن بشم در حال حاضر همچی درسته:

Image for post
Image for post

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

Image for post
Image for post

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

 در نهایت هم برای اطمینان از اینکه تاریخ پایان وظیفه که وارد کردیم درست وارد شده یا نه با متد assertNotNull ارزیابی می‌کنیم:

Image for post
Image for post

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

Image for post
Image for post

بار دیگه تست رو اجرا می‌کنیم که اینبار تست موفقیت آمیز بود!

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

Image for post
Image for post

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

Image for post
Image for post

برای این منظور هم باید از rule مربوطه و استفاده کنید برای آشنایی با اعتبارسنجی می‌توانید مقاله‌ای که در این زمینه نوشتیم رو مطالعه کنید:

نهایتا مجددا rule رو بصورت زیر ویرایش کردیم که نتیجه برای کلاس TaskRequest بصورت زیر شد:

public function rules()
{
    return [
        'title' => 'required',
        'description' => 'sometimes',
        'ended_at' => 'sometimes|date'
    ];
}

حالا برای نیازمندی بعدی در پروژه نمی‌خواهیم کاربر زمان وارد شده رو پیش از زمان فعلی وارد کنه. یعنی نمی‌خواهیم کاربر بتونه تاریخ اتمام یک وظیفه رو در گذشته وارد کنه!

Image for post
Image for post

و مجددا تست رو اجرا می‌کنیم که همانطور که انتظار میره خطا دریافت می‌کنیم که دلیلش همون ruleهاست پس بار دیگه به کلاس TaskRequest برمی‌گردیم و بصورت زیر ویرایش می‌کنیم که نتیجه تست‌هامون موفقیت آمیز میکنه!

public function rules()
{
    return [
        'title' => 'required',
        'description' => 'sometimes',
        'ended_at' => 'sometimes|date|after:now'
    ];
}

در گام بعدی می‌خواهیم از فرمت تاریخی که وارد کردیم اطمینان پیدا کنیم پس برای این منظور تست زیر رو می‌نویسیم:

Image for post
Image for post

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

Image for post
Image for post

برای مشاهده نوع این متغیر کد زیر رو داخل تست قرار دادم همانطور که مشاهده خواهید کرد فرمت تاریخ مورد نظر ما از نوع رشته هست.

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 باید انجام بشه:

Image for post
Image for post

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

ارزیابی Job

خب بیایید در ادامه یک job ایجاد کنیم:

php artisan make:job TaskJob

در مورد نحوه اجرای job و crontab برای مدیریت اجرای آنها در مقاله دیگری نوشتم که می‌تونید برای اطلاعات بیشتر به اون مراجعه کنید:

اما درون TaskJob من می‌خوام که در میان کاربران سیستم وظایفی که در حال حاضر تاریخ آنها گذشته ایمیل ارسال شود و پس از اون تاریخ پایان وظیفه مورد نظر رو null می‌کنیم، در اینصورت دیگه برای اون وظیفه ایمیل ارسال نمیشه البته بهتر می‌یود ستونی برای این مورد در نظر می‌گرفتم (روشی که در اینجا پیاده کردیم قطعا میشه بهتر نوشت اما اینجا فقط جنبه آموزشی داره پس بذارید همه چیز رو ساده در نظر بگیریم).

Image for post
Image for post

خب حالا یک کلاس Test جدید ایجاد می‌کنیم:

php artisan make:test JobTest

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

Image for post
Image for post

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

Image for post
Image for post

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

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

برای این منظور کافیه متد fake رو از کلاس Mail فراخوانی کنیم با این دستور تمامی عملیاتی که ارسال ایمیل دارند mock میشن! 

Image for post
Image for post

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

Image for post
Image for post

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

بیایید تست دیگری رو بنویسیم ولی اینبار خود TaskJob رو تقلید کنیم. قاعدتا اینبار انتظار داریم تعداد وظایفی که تاریخ اتمام آنها گذشته و null شدند برابر با صفر باشه:

Image for post
Image for post

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

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


در این آموزش با نحوه شبیه‌سازی عملیات ارسال ایمیل در جریان تست کردن و بطور کلی عملیات mocking آشنا شدید که روند ارزیابی فرآیند‌ها رو آسون‌تر میکنه و اجازه می‌ده دقیقا همون قسمتی که می‌خواهید رو در روند توسعه ارزیابی کنید.

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