یکی از مهمترین بخشهایی که در پروژههای نرم افزاری مطرح است تست نویسی است که معمولا همگی به بهانههای کمبود وقت و ضرورت سروقت تحویل دادن پروژه این بخش را نادیده میگیریم، تصور کنید در پروژه ای فعالیت میکنید که اعضای تیم هر کدام در بخشی از پروژه فعالیت میکنند اگر تستهای پروژه را نداشته باشیم برای هر ریلیز در واقع ریسک میکنیم و وقوع خطا در یک بخش باعث مشکل و ناهماهنگی در سایر بخشها نیز میشود.
امروز تصمیم گرفتم هم در این مقاله در رابطه با تست نویسی در لاراول صحبت کنم و هم اینکه برای یکبار این مفاهیم را ریویو کنم تا بتونم به درستی در پروژه ها استفاده کنم، همچنین زمانبندیی که برای هر تسک ارایه میدهم همراه با درنظر گرفتن زمان تست نویسی برای تسک موردنظر باشد. در واقع بهانه های کمبود وقت و … برای تست نویسی را یکبار برای همیشه کنار بذاریم.
- تست نویسی نه تنها اطمینان ما به کدهایمان را بالا میبرد بلکه باعث بهبود و رشد برنامه نویسی ما میشود.
- شما نمیتوانید کدهای پروژه تون رو توسعه بدید اگر از تغییر آن وحشت کنید و تمایلی نداشته باشید آن را تغییر دهید،
- تست نویسی اشتباهاتتون توی برنامه نویسی را بهتون نمایش میدهد.
- حقیقت Testing افزایش سرعت در توسعه است.
Unit Test
تستهای این بخش معمولا توسط توسعه دهنده نرمافزار نوشته میشود برای اطمینان از صحت عملکرد هر بخش از برنامه ای که نوشته میشود. در نرمافزارها معمولا برای هر Function یا متد که یک اقدام را انجام میدهد یک unit test نوشته میشود، اینکار به توسعه دهنده این اطمینان را میدهد که هر متد یا فانکشن به تنهایی به درستی کار میکند و باعث کاهش هزینه و زمان در توسعه پروژه در آینده میشود.
یک برنامه نویس معمولا برای اجرای Test Caseهای برنامه از Unit Test Frameworkهایی استفاده میکند که ابزارهای زیادی برای اینکار وجود دارند و ما برای PHP از PHP Unit استفاده میکنیم.
PHPUnit یک ابزار Unit Testing برای برنامهنویسان PHP است. این ابزار بخشهای کوچکی از کد را که یونیت نامیده میشوند را دریافت کرده، و هر یک را به صورت جداگانه تست میکند. این ابزار همچنین به توسعهدهندگان اجازه میدهد برای اثبات این موضوع که سیستم به طرز معینی رفتار میکند، از Assertion Methodهای از پیش تعریف شده استفاده نمایند. PHPUnit بیشتر برای Unit Test طراحی شده است که بتوانیم برای پروژه های خود در سطح متوسط از آن استفاده کنیم ولی سادگی و انعطاف پذیری آن باعث میشود بتوانیم از آن در سطح گسترده تر نیز استفاده کنیم.
نکته: Unit Testing به ما اجازه میدهد کد خود را بعدا Refactor کنیم، و مطمئن شویم که نرمافزار همچنان درست کار میکند. این رویه برای نوشتن Test Caseها برای تمام Functionها و Methodهاست تا زمانیکه یک تغییر سبب بروز یک Fault(عیب) شد، بتوان آنرا به سرعت شناسایی و رفع نمود.
- Unit Test Caseها باید مستقل باشند. در مورد هر افزایش یا تغییر در نیازمندیها(Requirement)، نباید Unit Test Caseها تحت تاثیر قرار گیرند.
- در هر لحظه فقط یک کد را تست کنید.
- دستورالعملهای نامگذاری واضح و نامتناقض را برای Unit Testها دنبال کنید در صورت تغییر در کد هر ماژول، اطمینان حاصل کنید که یک Unit Test Case متناظر برای ماژول وجود دارد، و ماژول موردنظر قبل از تغییر در پیادهسازی، تستها را Pass میکند.
- باگهای شناسایی شده در طول Unit Testing باید قبل از ارسال به فاز بعدی رفع شوند.
- رویکرد “Test as You Code”(همینطور که کد میزنید، تست کنید) را اتخاذ کنید. نوشتن کد زیاد و بدون تست به معنی مسیرهای بیشتری است که باید برای کشف خطاها بررسی شوند.
Feature Test
تستهای این بخش معمولا توسط یک شخص دیگر تحت عنوان Tester انجام میشود. Feature Testها به تست ویژگیها و تغییرات جدید نرمافزار میپردازند برای مثال وقتی ما به نرم افزار یک ویژگی جدید اضافه کردیم با استفاده از این تستها از درستی این ویژگیها و تغییرات مطمئن میشویم.
- این تست به اعتبارسنجی ویژگیها و تغییرات جدید نرم افزار کمک میکند.
- نرم افزار با تنظیمات مختلف مورد بررسی قرار میگیرد.
- باگهای احتمالی در مراحل اولیه انتشار شناسایی میشوند.
- تمام بخشهای نرم افزار قابل تست هستند.
- ادغام نرم افزار با بخشهای مختلف در این تستها مورد بررسی قرار میگیرند.
- این تستها به رفع باگها و افزایش کیفیت نرمافزار کمک زیادی میکند.
برای کمک به توسعه دهندگان لاراول امکانی در لاراول فراهم شده که بتوانیم به سادگی کدهای Unit Test را برای تست بخشهای پیچیده اپلیکیشن استفاده کنیم.
اگر کد زیر را ببینید متوجه میشویم که ما تنظیماتی را در php-unit انجام داده ایم، با استفاده از colors=true تعیین کردیم که نتایج تست به صورت رنگی در ترمینال نمایش داده شود و با استفاده از تگ directory مسیر تستهای پروژه را مشخص کردیم. همچنین با استفاده از stopOnFailure=”true” میتوانیم تعیین کنیم که بعد از هر خطا ادامه اجرا متوقف شود.
ایجاد Test Caseها در لاراول
ما میتوانیم یا به صورت دستی فایلهای تست را ایجاد کنیم و یا با استفاده از دستورات زیر میتوانیم این تستها را ایجاد کنیم.
// Feature Tests
php artisan make:test SampleTest
// Unit Test
php artisan make:test SampleTest --unit
بعد از اجرای کد فوق یک کلاس نمونه تست به صورت زیر ایجاد میشود، نکته مهم این است که اسم متد با نام test شروع میشود، هر متدی که با این اسم شروع نشود نادیده گرفته میشود.
class SampleTest extends TestCase
{
/**
* A basic unit test example.
*
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
}
همچنین میتوانیم اسم فانکشنها را بدون test بنویسیم ولی قبل از اسم متد @test را قرار دهیم.
class SampleTest extends TestCase
{
/**
* A basic unit test example.
*
* @test
* @return void
*/
public function Example()
{
$this->assertTrue(true);
}
}
حالا که فایلهای تست را ایجاد کردیم برای مشاهده پیکربندی PHPUnit فایل phpunit.xml را که لاراول به صورت پیشفرض دارد را بررسی میکنیم. PHPUnit به صورت پیشفرض در دایرکتوری به دنبال فایلی به نام phpunit.xml یا phpunit.xml.dist میگردد و مطابق با آن و بر اساس پیکربندی این فایل تستها را اجرا میکند.
فایل phpunit.xml را به صورت زیر داریم که مهمترین بخش آن بخش testsuite است که در داخل تگ directory مسیر فایلهای تست را مشخص میکند.
اجرای تست پروژه
وقتی شروع به تست نویسی میکنید با استفاده از دستورات زیر میتوانید تست را اجرا کنید.
vendor/bin/phpunit
phpunit
در صورتیکه همه چیز درست باشد و مشکلی در پاس شدن تستها وجود نداشته باشد نتیجهای مانند تصویر زیر را مشاهده میکنید.
تستهای پروژه داخل فولدر tests قرار میگیرند، که داخل فولدر tests شما دو فولدر به اسمهای Feature و Unit مشاهده میکنید که برای تفکیک Feature testها و Unit testها هستند. تمام فایلهای تست باید به نام Test.php ختم شوند و هر فایل که Test.php را در انتهای نام خود نداشته باشد نادیده گرفته میشود.
همانطور که در تصویر بالا مشاهده کردید با استفاده از کامند phpunit میتوانیم تست را اجرا کنیم. در تصویر زیر را با ارسال یک آپشن به اجرای تست مشاهده میکنید که اجرای تست با نمایش داکیومنتهای تست زیباتر و قابل فهمتر خواهد بود.
همانطور که اشاره کردیم با اجرای کامند phpunit میتوانیم تست پروژه را اجرا کنیم ولی با اجرای کامند php artisan test میتوانیم به سبک زیر تست را اجرا کنیم.
گروه بندی Test Caseها در لاراول
ما میتوانیم متدهایی که در Test Caseها مینویسیم را گروه بندی کنیم، و با استفاده از کامند php artisan test –group skip فقط متدهایی که تگ skip را دارند اجرا میشوند و با استفاده از کامند php artisan test –exclude skip متدهایی که تگ skip دارند را اجرا نمیکند.
به صورت زیر میتوانیم متدها را گروه بندی کنیم.
class SampleTest extends TestCase
{
/**
* A basic unit test example.
* @group skip
* @return void
*/
public function testExample()
{
$this->assertTrue(true);
}
}
Assertionهای PHPUnit
- assertTrue()
- assertFalse()
- assertEquals()
- assertNull()
- assertContains()
- assertCount()
- assertEmpty()
- assertStatus()
متد assertStatus
از این متد برای بررسی وضعیت Routی که داریم استفاده میکنیم. برای مثال ما با استفاده از این Route به صورت زیر میتوانیم چک کنیم که هدر صفحه مورد نظر (HTTP status code) چه statusی دارد؟
public function testBasicTest()
{
$response = $this->get('/');
$response->assertStatus(200);
}
متدهای assertTrue و assertFalse
با استفاده از این متدها میتوانیم مشخص کنیم که یک مقدار مساوی است با True یا False. پس برای موارد از این Assertionها استفاده میکنیم که مطمئن باشیم که مقدار نهایی True یا False است.
متد assertEquals
با استفاده از این متد میتوانیم دو مقدار را باهم مقایسه کنیم که مساوی هستند یا خیر. این متد دو پارامتر به عنوان ورودی میگیرد پارامتر اول مقداری که مورد انتظار است و پارامتر دوم مقدار واقعی.
متد assertNull
همانطور که از اسم این متد پیداست با استفاده از این متد مشخص میکنیم که خروجی، یک مقدار Null است یا خیر.
متد assertContains
این متد با آرایه ها کار میکند و چک میکند که مقدار موردنظر در آرایه وجود دارد یا خیر.
متد assertCount
این متد تعداد آیتم های داخل آرایه را با مقدار داده شده بررسی می کند.
متد assertEmpty
این متد خالی بودن آرایه را تعیین می کند.
تا به اینجا ما ۸ تابع(Assertion) ساده PHPUnit را بررسی کردیم با استفاده از این توابع ساده میتوانیم تستهای پیچیده ای را بنویسیم.
با استفاده از این توابع ما میتوانیم بخشهای مختلف برنامه را تست کنیم ولی مساله ای که وجود دارد این است که ما میخواهیم بخشهای مختلف برنامه مثلا Viewها و در دسترس بودن صفحات و … را تست کنیم برای این موارد هم از Helperهای لاراول استفاده میکنیم که کمک زیادی به ما میکند.
شما میتوانید در وبسایت PHP-UNIT لیست کامل Assertionهایی که برای phpunit داریم را مطالعه کنید.
Customizing Request Headers
هنگام تست میتوانیم هدرهای مشخصی را به Route موردنظر ارسال کنیم.
public function test_interacting_with_headers()
{
$response = $this->withHeaders([
'X-Header' => 'Value',
])->post('/user', ['name' => 'Sally']);
$response->assertStatus(201);
}
و یا کوکیهای موردنظرمان را دریافت کنیم.
public function test_interacting_with_cookies()
{
$response = $this->withCookie('color', 'blue')->get('/');
$response = $this->withCookies([
'color' => 'blue',
'name' => 'Taylor',
])->get('/');
}
همچین میتوانیم به صورت زیر با Sessionها کار کنیم:
public function test_interacting_with_the_session()
{
$response = $this->withSession(['banned' => false])->get('/');
}
با استفاده از متد actingAs میتوانیم کاربر را لاگین کنیم و تستهایی را انجام دهیم.
public function test_an_action_that_requires_authentication()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->withSession(['banned' => false])
->get('/');
}
همچنین میتوانیم Guard name را به متد actingAs پاس دهیم:
$this->actingAs($user, 'api')
دیباگ کردن ریسپانس (Debugging Responses)
public function testBasicTest()
{
$response = $this->get('/');
$response->dumpHeaders();
$response->dumpSession();
$response->dump();
}
Helperهای لاراول برای تست
Laravel Environment For PHPUnit Test
وقتی که تستها را اجرا میکنیم لاراول به صورت پیشفرض فایل .env و مقادیر Variableهای موردنیاز پروژه را درنظر میگیرد، لاراول امکانی را فراهم کرده است که میتوانیم محیط Envirement متفاوتی برای Test Caseهای پروژه داشته باشیم، کافی است فایلی به اسم .env.testing را ایجاد کنیم و البته قبل از اجرای تست حتما کش کانفیگ را پاک کنید. (php artisan config:clear)
همچنین برای اجرای کامندهای Artisan پروژه برای محیط تست کافی است آپشن –env=testing را در انتهای کامند وارد کنیم.
Test Driven Development – TDD
احتمالا عنوانهایی تحت توسعه پروژه به صورت TDD را شنیده اید، به این معناست که برای توسعه یه پروژه نرم افزاری ابتدا Test Caseهای آن را بنویسید و بعد از آن خود متد را پیاده سازی کنید، مزیت این سبک از توسعه نرم افزار این است که ما ابتدا تمام احتمالات و موارد را در نظر میگیریم و به سبکی کد مینویسیم که برنامه ما تمام تستها را به درستی پاس کند.
نکته مهم: همانطور که میدانیم برای متدهایی که در فایلهای Test Case مینویسیم اسم متد باید با کلمه test شروع شود، وقتی شروع به نوشتن تستهای برنامه کردید قراردادی را با خود مشخص کنید که به کدام سبک نامگذاری متدهای تست را پیش ببرید. مثلا به صورت testBasicExample یا test_interacting_with_the_session.
باهم شروع به تست نویسی کنیم!
تا به اینجای کار با Assertionها و Unit testing و … آشنا شدیم الان در محیط واقعی پروژه شروع به تست نویسی میکنیم! اولین مساله ای که برای ما مطرح است این است که ما وقتی هنگام تست نویسی رکوردی را ایجاد کنیم و یا با ایجاد رکورد ویژگیهایی را به آن نسبت دهیم چه راهی را باید پیش بگیریم که با دیتاهای واقعی در پروژه ما ترکیب نشود! و باعث ایجاد خطا نشود و …
در PHPUnit ما میتوانیم از DatabaseTransactions استفاده کنیم برای استفاده از آن کافی است که داخل فایل test آن را use کنیم و تست را اجرا کنیم، اگر به trait اصلی DatabaseTransactions نگاهی بندازیم متوجه میشویم که با هربار تست رکوردهایی که ایجاد شده اند را rolleBack میکند که با اینکار باعث میشود که رکوردهای تست ما داخل دیتابیس وجود نداشته باشد یعنی بعد از اجرا آنها را از دیتابیس حذف میکند.
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Modules\User\Entities\User;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use DatabaseTransactions;
/**
* A basic test example.
* @test
* @return void
*/
public function basicTest()
{
$new_user = User::create([
'first_name' => "Mekaeil",
'last_name' => "Andisheh",
'email' => "m@gmail.com",
'mobile' => "091230123",
'password' => "123123123",
]);
$this->assertEquals("Mekaeil Andisheh", $new_user->full_name);
}
}
وقتی که ما در مورد تست صحبت میکنیم در واقع ما باید سعی کنیم تمام بخشهای برنامه را با استفاده از تست پوشش دهیم و این شامل فایلهای مایگریشن نیز میشود که اگر در آینده خواستیم پروژه را بر روی پلتفرم جدید نصب کنیم و یا اینکه مطمئن شویم تمام ساختار دیتابیس بر روی سرور و لوکال ما یکی است باید این بخش را نیز پوشش دهیم.
همانطور که در نمونه تست قبل اشاره کردیم با استفاده از DatabaseTransactions میتوانیم رکوردهای ایجاد شده را RolleBack کنیم ولی از نظر من این یک کار اصولی و درستی نیست. برای رفع این مشکل اگر به تصویری که در ابتدای مقاله از فایل phpunit.xml گذاشتم نگاهی بندازید دو خط کامنت شده را مشاهده میکنید. این دو خط را از کامنت خارج میکنیم و برای PHPUnit تعریف میکنیم که ما از sqlite و memory استفاده میکنیم در واقع ما یک دیتابیس مجازی را برای اجرای تستها درنظر میگیریم همچنین از تریت DatabaseMigrations استفاده میکنیم که تمام جداول را fresh میکند.
دقت کنید که فایل phpunit شما دو مقدار DB_CONNECTION=sqlite و DB_DATABASE=:memory: را داشته باشد و کش را پاک کرده باشید در غیر اینصورت دیتابیس شما و دیتاهای شما از بین میرود.
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Modules\User\Entities\User;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use DatabaseMigrations;
/**
* A basic test example.
* @test
* @return void
*/
public function basicTest()
{
$new_user = User::create([
'first_name' => "Mekaeil",
'last_name' => "Andisheh",
'email' => "m@gmail.com",
'mobile' => "091230123",
'password' => "123123123",
]);
$this->assertEquals("Mekaeil Andisheh", $new_user->full_name);
}
}
رفع یک باگ اجرای تست در لاراول ۸:
به احتمال زیاد اگر در لاراول ۸ تست را اجرا کنید با خطایی مثل: Call to a member function connection() on null برمیخورید برای رفع این مورد در فایلی که تستهایتان را نوشته اید و TestCase را use کرده اید که به این شکل است: use PHPUnit\Framework\TestCase; اگر آن را به use Tests\TestCase; تغییر دهید مشکل حل میشود.
نکته: در صورت تمایل شما میتوانید یک درایور جدید در مسیر فایل config/database بسازید و اسم آن را مثلا testing بذارید و مقادیر آن را از هر نوع دیتابیسی که میخواهید کپی کنید مثلا MySql و از آن برای دیتابیس تست خود استفاده کنید و ازحالت memory آن را خارج کنید، بعد از اینکار باید مقدار DB_CONNECTION=sqlite در فایل phpunit.xml را به مقداری که در فایل database تعریف کردید تغییر دهید مثلا: DB_CONNECTION=testing
نکته۲: وقتی که از DatabaseMigrations استفاده میکنیم و DB_CONNECTION=sqlite و DB_DATABASE=:memory: است هنگام تست نویسی شاید با خطا مواجه شویم که مقداری را که با استفاده از متد make() ایجاد کرده ایم موقع استفاده ار متدهای See تست با شکست روبرو شود! در این حالت به جای make از متد create استفاده کنید تا تستها به درستی پاس شوند.
یک تجربه:
در صورتیکه تست را اجرا کردید و با خطاهایی همچون این مواجه شدید که جدول موردنظر وجود ندارد و … مشکل از ساختار فایلهای مایگریشن شما و اولویت و ترتیب اجرای فایلهاست. در صورتیکه برای این موضوع جستجو کنید در برخی وبسایتها به شما توصیه میکنند که داخل TestCase موردنظرتان RefreshDatabase را use کنید تا مشکل حل شود اگر اینکار را انجام دهید و یادتون بره که کامند config:clear بزنید کل دیتاهای شما پاک میشود! پس قبل از اجرای تست و راه اندازی بیس اولیه تستها ابتدا از دیتابیس بک آپ بگیرید و مطمئن شوید که config درست کش شده است.
من پیشنهاد میکنم که از DatabaseMigrations استفاده کنید و تستهای خود را اجرا کنید و مرحله به مرحله مشکلات مایگریشنها را پیدا کنید تا به نتیجه نهایی برسید. برای مثال در تصویر زیر یک اشتباه از rolleback یه ستون از نوع enum را مشاهده میکنید که موقع اجرای تست نمایش داده میشود، پس با تست نویسی و استفاده از DatabaseMigrations مطمئن میشویم که بیشتر بخشهای برنامه به درستی کار میکند. همانطور که میدانید هنگام rolleback نمیتوانیم به این روش یه ستون enum را تغییر دهیم.
اگر میخواهید از sqlite برای محیط تست استفاده کنید پیشنهاد میکنم از RefreshDatabase استفاده کنید به این خاطر که در صورت استفاده از DatabaseMigrations با خطایی همچون
SQLite doesn’t support dropping foreign keys (you would need to re-create the table) روبرو میشوید. یا اینکه میتوانید به صورت زیر از mysql استفاده کنید و یک دیتابیس مجزا برای محیط test ایجاد کنید.
http://mekaeil.me/laravel-php-unit-test-part-2/