این مطلب سال ۹۹ نوشته شده و از وبلاگم به ویرگول منتقل شده است، نکاتی مفید رو دربر داره به همین خاطر منتشر شد.
یکی از مهمترین بخشهایی که در پروژههای نرم افزاری مطرح است تست نویسی است که معمولا همگی به بهانههای کمبود وقت و ضرورت سروقت تحویل دادن پروژه این بخش را نادیده میگیریم، تصور کنید در پروژه ای فعالیت میکنید که اعضای تیم هر کدام در بخشی از پروژه فعالیت میکنند اگر تستهای پروژه را نداشته باشیم برای هر ریلیز در واقع ریسک میکنیم و وقوع خطا در یک بخش باعث مشکل و ناهماهنگی در سایر بخشها نیز میشود.
امروز تصمیم گرفتم هم در این مقاله در رابطه با تست نویسی در لاراول صحبت کنم و هم اینکه برای یکبار این مفاهیم را ریویو کنم تا بتونم به درستی در پروژه ها استفاده کنم، همچنین زمانبندیی که برای هر تسک ارایه میدهم همراه با درنظر گرفتن زمان تست نویسی برای تسک موردنظر باشد. در واقع بهانه های کمبود وقت و ... برای تست نویسی را یکبار برای همیشه کنار بذاریم.
تستهای این بخش معمولا توسط توسعه دهنده نرمافزار نوشته میشود برای اطمینان از صحت عملکرد هر بخش از برنامه ای که نوشته میشود. در نرمافزارها معمولا برای هر 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(عیب) شد، بتوان آنرا به سرعت شناسایی و رفع نمود.
تستهای این بخش معمولا توسط یک شخص دیگر تحت عنوان Tester انجام میشود. Feature Testها به تست ویژگیها و تغییرات جدید نرمافزار میپردازند برای مثال وقتی ما به نرم افزار یک ویژگی جدید اضافه کردیم با استفاده از این تستها از درستی این ویژگیها و تغییرات مطمئن میشویم.
برای کمک به توسعه دهندگان لاراول امکانی در لاراول فراهم شده که بتوانیم به سادگی کدهای Unit Test را برای تست بخشهای پیچیده اپلیکیشن استفاده کنیم.
اگر کد زیر را ببینید متوجه میشویم که ما تنظیماتی را در php-unit انجام داده ایم، با استفاده از colors=true تعیین کردیم که نتایج تست به صورت رنگی در ترمینال نمایش داده شود و با استفاده از تگ directory مسیر تستهای پروژه را مشخص کردیم. همچنین با استفاده از stopOnFailure="true" میتوانیم تعیین کنیم که بعد از هر خطا ادامه اجرا متوقف شود.
ما میتوانیم یا به صورت دستی فایلهای تست را ایجاد کنیم و یا با استفاده از دستورات زیر میتوانیم این تستها را ایجاد کنیم.
// 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ها مینویسیم را گروه بندی کنیم، و با استفاده از کامند 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); } }
از این متد برای بررسی وضعیت Routی که داریم استفاده میکنیم. برای مثال ما با استفاده از این Route به صورت زیر میتوانیم چک کنیم که هدر صفحه مورد نظر (HTTP status code) چه statusی دارد؟
public function testBasicTest() { $response = $this->get('/'); $response->assertStatus(200); }
با استفاده از این متدها میتوانیم مشخص کنیم که یک مقدار مساوی است با True یا False. پس برای موارد از این Assertionها استفاده میکنیم که مطمئن باشیم که مقدار نهایی True یا False است.
با استفاده از این متد میتوانیم دو مقدار را باهم مقایسه کنیم که مساوی هستند یا خیر. این متد دو پارامتر به عنوان ورودی میگیرد پارامتر اول مقداری که مورد انتظار است و پارامتر دوم مقدار واقعی.
همانطور که از اسم این متد پیداست با استفاده از این متد مشخص میکنیم که خروجی، یک مقدار Null است یا خیر.
این متد با آرایه ها کار میکند و چک میکند که مقدار موردنظر در آرایه وجود دارد یا خیر.
این متد تعداد آیتم های داخل آرایه را با مقدار داده شده بررسی می کند.
این متد خالی بودن آرایه را تعیین می کند.
تا به اینجا ما ۸ تابع(Assertion) ساده PHPUnit را بررسی کردیم با استفاده از این توابع ساده میتوانیم تستهای پیچیده ای را بنویسیم.
با استفاده از این توابع ما میتوانیم بخشهای مختلف برنامه را تست کنیم ولی مساله ای که وجود دارد این است که ما میخواهیم بخشهای مختلف برنامه مثلا Viewها و در دسترس بودن صفحات و ... را تست کنیم برای این موارد هم از Helperهای لاراول استفاده میکنیم که کمک زیادی به ما میکند.
شما میتوانید در وبسایت PHP-UNIT لیست کامل Assertionهایی که برای phpunit داریم را مطالعه کنید.
هنگام تست میتوانیم هدرهای مشخصی را به 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')
public function testBasicTest() { $response = $this->get('/'); $response->dumpHeaders(); $response->dumpSession(); $response->dump(); }
وقتی که تستها را اجرا میکنیم لاراول به صورت پیشفرض فایل .env و مقادیر Variableهای موردنیاز پروژه را درنظر میگیرد، لاراول امکانی را فراهم کرده است که میتوانیم محیط Envirement متفاوتی برای Test Caseهای پروژه داشته باشیم، کافی است فایلی به اسم .env.testing را ایجاد کنیم و البته قبل از اجرای تست حتما کش کانفیگ را پاک کنید. (php artisan config:clear)
همچنین برای اجرای کامندهای Artisan پروژه برای محیط تست کافی است آپشن --env=testing را در انتهای کامند وارد کنیم.
احتمالا عنوانهایی تحت توسعه پروژه به صورت 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); } }
به احتمال زیاد اگر در لاراول 8 تست را اجرا کنید با خطایی مثل: 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 ایجاد کنید.
بر اساس نوشته Robert C. Martin در کتاب Clean Code، تستهای تمیز پنج قانون را دنبال میکنند که عبارت F.I.R.S.T را تشکیل داده اند.
تست باید سریع باشد، آنها باید سریع عمل کنند. وقتی تستها کند اجرا میشوند نمیخواهید آنها را به صورت مداوم اجرا کنید. اگر آنها را به صورت مداوم اجرا نکنید، به زودی مشکلاتی پیدا خواهید کرد که به راحتی حل نخواهند شد. شما به راحتی تمیزکردن کد را احساس نخواهید کرد. در نهایت کد شروع به پوسیدگی خواهد کرد.
تستها نباید به یکدیگر وابسته باشند. یک تست نباید شرایط تست بعدی را تنظیم کند. شما باید بتوانید هر تست را به صورت مستقل اجرا کنید و آنها را در هر جهتی که دوست دارید انجام دهید. وقتی تستها به یکدیگر متکی هستند، اولین شکست، یک آبشار از شکستهای پایین دست را ایجاد میکند، تشخیص را مشکل میکند و نقضهای پایین دست را پنهان میکند.
تستها باید در هر محیط تکرار شوند. شما باید بتوانید آزمایشها را در محیط تولید، در محیط QA و در لپ تاپ خود در حالی که در خانه، در قطار یا هرجایی بدون شبکه هستید انجام دهید. اگر تستهای شما در هر محیط تکرار نشوند، همیشه بهانه ای برای دلیل شکست آنها خواهید داشت.
تستها باید یک خروجی بولین داشته باشند که یا عبور میکنند یا شکست میخورند. شما نباید از طریق یک فایل log بخوانید و ببینید که آیا تستها پاس شده اند یا خیر. شما نباید به صورت دستی دو فایل متنی مختلف را مقایسه کنید تا ببینید تستها پاس میشوند یا خیر. اگر تستها اعتبار خود را تایید نکنند، پس این شکست میتواند درونی باشد و اجرای تست میتواند نیاز به ارزیابی دستی طولانی توسط کاربر داشته باشد.
تستها باید به موقع نوشته شوند. تستهای واحد باید قبل از کد تولید نوشته شوند که باعث پاس شدن آنها میشود. اگر تستها را بعد از کد تولید بنویسید ممکن است تصمیم بگیرید که برخی از کدهای تولید برای تست کردن طاقت فرسا باشند و یا تستها را به گونه ای بنویسید که کد تولید را پاس کند و حالتهای مختلف تست را درنظر نگیرید.
در نهایت همانطور که توسعه پروژه و کدهای تولید پروژه مهم هستند شاید تستها مهمتر هم باشند زیرا تستها انعطاف پذیری، قابلیت نگهداری و استفاده مجدد کدهای تولید را حفظ میکنند بنابراین تستهای خود را همیشه تمیز نگه دارید روی آنها کار کنید تا مختصر و رسا و واضح باشند. اگر شما اجازه دهید که تستها پوسیده شوند کد شما نیز پوسیده خواهد شد. تستهای خود را تمیز نگه دارید.
این بخش از مقاله برگرفته از کتاب Clean Code نوشته Robert C. Martin است. میتوانید تسخه فارسی کتاب Clean Code را تهیه کنید.