تست نویسی در لاراول – بخش دوم

در مقاله قبل در رابطه با اصول اولیه تست نویسی در لاراول صحبت کردیم. در این مقاله برخی موارد را در ابتدا تکمیل تر میکنم و بعد در رابطه با سایر ویژگی ها و امکاناتی که داریم صحبت میکنیم.

 

در ابتدا با این سوال شروع میکنم،

برای چه بخش‌هایی باید تست بنویسیم و چطور تشخیص دهیم که تست‌ها را در کدام بخش Unit Test یا Feature Test بنویسم؟

برای تمام بخش‌های پروژه مثل Modelها، Controllerها و Repository Fileها و هر بخشی که یک فانکشنالیتی به پروژه ما اضافه میکند که کاربر از آن استفاده میکند و یا به صورت غیر مستقیم دیتاهایی را برای کاربر فراهم میکند باید تست نوشته شود. وقتی یک Entity یا Model به پروژه اضافه میشود برای تمام Logicهایی که در پروژه داریم test مینویسیم.

Unit Testها پایین ترین level تست نویسی هستند که برای هر فانکشنالیتی یا متد نوشته می‌شوند مثلا برای تمام Relationها و logicهایی که برای Model تعریف شده است باید Unit Test بنویسیم.

Integration Testها را وقتی درنظر میگیریم که یک فرآیند داریم مثلا مقادیری را داخل دیتابیس بررسی میکنیم و چندین بخش از پروژه باهم در ارتباط هستند و ما انتظار یک نوع خروجی داریم و یا با یک API توی دیتابیس مقادیری را بررسی میکنیم.

With feature tests, you are testing the application by interacting with it just like a real user would do. So they are integration tests.

by definition feature and integration testing are not necessarily the same.

Unit tests usually test the smallest unit in your code which is most likely a method or function. Integration tests should make sure that more than one unit or one or more modules work together as expected. A feature test is usually an end-to-end test, e.g. you test an API endpoint via HTTP request and assert its response.

The API request will go through all layers of your application, for instance, controller, models, DBAL, DBMS.

We run quite a big, multi-tenanted Laravel application in my company and we have the following test suites: * Unit tests * Http tests for API endpoints (end to end, without DB mocks) * Browser tests w/ Dusk (end to end, without DB mocks)

All external / 3rd party calls (i.e. Facebook API, email service provider) are mocked in the tests.

نکته: فضای هر تست باید ایزوله باشد. برای مثال وقتی یک داده ای داریم که در هر متد تست باید استفاده کنیم به ازای هر تست باید اجرا شود و هر تست باید ایزوله و کاملا مستقل از سایر تستها باشد.

 

مطالعه بیشتر: Difference between feature test and integration testing

 

 Data Provider در تست نویسی

وقتی که برای بخشهایی از پروژه تست مینویسیم در حالتهای مختلف دیتاهایی را پاس میدهیم و بعد با Assertion چک میکنیم که آیا مقدار موردانتظار ما بوده است یا خیر. در این حالت متدهای مختلفی را ایجاد میکنیم چراکه بهتر است حداقل Assertها را در هر متد تست داشته باشیم و یا به عبارتی دیگر سعی کنیم حتی الامکان برای هر تست یک Assertion داشته باشیم.

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


/*
 * @test
 * @dataProvider categoryProvider
 */
public function the_categories_posts($input, $expected)
{
    /// do action
    $this->assertSame($expected, $input);
}

public function categoryProvider()
{
    return [
      ['input', ['expected_1', 'expected_1']],
      ['another_input', ['expected_1', 'expected_1']],
        ///...
    ];
}

 

بررسی یک Exception در تست

در برخی موارد ما نیاز داریم مطمئن شویم که اگر یک حالت خاصی رخ داد مثلا وقتی اعتبار پنل sms ما موجود نبود، کاربر موجودی کافی نداشت و یا هر حالت دیگری بسته به پروژه میتواند رخ دهد ما در این شرایط باید یک throw exception در برنامه داشته باشیم. در این حالت وقتی تست مینویسم در این حالت با خطا موجه می‌شویم پس باید قبل از اینکه به این حالت برسیم باید اعلام کنیم که انتظار یک throw exception را داریم و حتی نوع آن را هم تعریف میکنیم.


/** @test **/
public function submit_new_message()
{
    $this->expectException(\Exception::class);
    /// do action
}

چگونه برای APIهای واقعی تست بنویسیم؟!

فرض کنید که میخواهیم برای پرداخت درگاه بانک تست بنویسیم و مطمئن شویم که فانکشنالیتی آن از سمت ما به درستی کار میکند، همانطور که میدانید منطقی نیست که هربار API موردنظر یا sandbox آن را فراخوانی کنیم و آن را تست کنیم. برای این کار مفهومی به اسم Dummy Test مطرح می‌شود. ما برای اینکار یک درگاه Fake ایجاد میکنیم و صرفا مطمئن می‌شویم که فانکشنالیتی آنها به درستی کار میکند.

وقتی که ما برای هر متد یک تست مینویسیم با استفاده از متد زیر مشخص میکنیم که ما این بخش از تست را skip میکنیم و متوجه هستیم که نمیخواهیم آن را اجرا کنیم و در عوض در متدهای بعد از درگاه Fake استفاده میکنیم.


$this->markTestSkipped('We dont want to pay...');

در نمونه کد زیر که JEFFREY WAY در دوره PHP Testing Jargon مطرح کرده است به صورت زیر تست درگاه strip را بیان میکند.

A Dummy is a Stand-in for the Real Thing

اما راه حل بهتر آن این است که از phpunit بخواهیم یک Mock از درگاه برای ما ایجاد کند!


class StubTest extends TestCase
{
    public function testStub()
    {
        // Create a stub for the SomeClass class.
        $stub = $this->createMock(SomeClass::class);
    }
}

در واقع ما از Mock استفاده کردیم که از نوع کلاسی که میخواهیم برای ما یک نمونه Fake ایجاد کند اما اگر بخواهیم کلاس Fake ایجاد شود و مقادیری را هم return کند و یا روی برخی متدهای آن کاری انجام دهیم باید چطور پیاده سازی کنیم؟!

در واقع اگر برای نوع کلاس مورد نظر ما متدهایی هم داشته باشیم Mock مقادیر آنها را null درنظر میگیرد چونکه در نمونه کد فوق ما فقط عنوان کردیم که از نوع کلاس برای ما ایجاد کند! حالا اگر بخواهیم مثلا برای متد create یک پاسخ خاصی به return شود باید برای آن تعریف کنیم.

به دو صورت میتوانیم این مورد را پیاده سازی کنیم یا به صورت کد زیر با استفاده از Mock تعریف کنیم که برای متد create چه مقداری را return کند و یا یک کلاس Fake مثلا به اسم GatewayStub ایجاد کنیم و از ان کلاس استفاده کنیم و متدها را تعریف و نوعی که میخواهیم را return  کند.


class StubTest extends TestCase
{
    public function testStub()
    {
        // Create a stub for the SomeClass class.
        $stub = $this->createMock(SomeClass::class);

        // Configure the stub.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Calling $stub->doSomething() will now return
        // 'foo'.
        $this->assertEquals('foo', $stub->doSomething());
    }
}

 

 

 

یک تست تمیز چه ویژگی‌هایی دارد؟

بر اساس نوشته Robert C. Martin در کتاب Clean Code، تستهای تمیز پنج قانون را دنبال میکنند که عبارت F.I.R.S.T را تشکیل داده اند.

Fast

تست باید سریع باشد، آنها باید سریع عمل کنند. وقتی تستها کند اجرا میشوند نمیخواهید آنها را به صورت مداوم اجرا کنید. اگر آنها را به صورت مداوم اجرا نکنید، به زودی مشکلاتی پیدا خواهید کرد که به راحتی حل نخواهند شد. شما به راحتی تمیزکردن کد را احساس نخواهید کرد. در نهایت کد شروع به پوسیدگی خواهد کرد.

Independent

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

Repeatable

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

Self-Validating

تستها باید یک خروجی بولین داشته باشند که یا عبور میکنند یا شکست میخورند. شما نباید از طریق یک فایل log بخوانید و ببینید که آیا تستها پاس شده اند یا خیر. شما نباید به صورت دستی دو فایل متنی مختلف را مقایسه کنید تا ببینید تستها پاس می‌شوند یا خیر. اگر تستها اعتبار خود را تایید نکنند، پس این شکست میتواند درونی باشد و اجرای تست میتواند نیاز به ارزیابی دستی طولانی توسط کاربر داشته باشد.

Timely

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

 

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

این بخش از مقاله برگرفته از کتاب Clean Code نوشته Robert C. Martin است. میتوانید تسخه فارسی کتاب Clean Code را تهیه کنید.

 

Comments