
اگر چند ماه روی یک پروژه لاراولی کار کرده باشید، احتمالا حداقل یک بار این تجربه را داشتهاید: یک قابلیت جدید اضافه میکنید، همه چیز درست کار میکند، کد را دپلوی میکنید و ناگهان بخشی از سیستم که هیچ ارتباط مستقیمی با تغییرات شما نداشته از کار میافتد.
این اتفاق معمولا نتیجه نبود تستهای خودکار است.
بسیاری از توسعهدهندگان تازهکار تصور میکنند تستنویسی فقط برای پروژههای بزرگ یا تیمهای چند ده نفره است. اما واقعیت این است که حتی یک پروژه شخصی هم بدون تست به مرور زمان به کدی شکننده تبدیل میشود.
لاراول از همان روزهای ابتدایی خود توجه ویژهای به تستنویسی داشته است. وجود PHPUnit به صورت پیشفرض، ابزارهای قدرتمند Mocking، امکان تست پایگاه داده و در سالهای اخیر پشتیبانی فوقالعاده از Pest PHP باعث شده لاراول یکی از بهترین اکوسیستمهای تستنویسی در دنیای PHP را داشته باشد.
در این مقاله به صورت کامل یاد میگیریم:
تستنویسی چیست و چرا اهمیت دارد
تفاوت PHPUnit و Pest PHP
انواع تستها در لاراول
ساخت Unit Test و Feature Test
تست پایگاه داده
Mock و Fake کردن سرویسها
تست APIها
تست احراز هویت
بهترین شیوههای تستنویسی در پروژههای واقعی
تستنویسی فرآیند نوشتن کدهایی است که رفتار سایر بخشهای برنامه را بررسی میکنند.
به جای اینکه بعد از هر تغییر برنامه را دستی اجرا کنیم و امیدوار باشیم چیزی خراب نشده باشد، مجموعهای از تستها را اجرا میکنیم تا مطمئن شویم همه چیز مطابق انتظار کار میکند.
مثلا فرض کنید متدی برای محاسبه مالیات داریم:
class TaxCalculator { public function calculate(float $amount): float { return $amount * 0.09; } }
میتوانیم برای آن تست بنویسیم:
public function test_tax_calculation() { $calculator = new TaxCalculator(); $this->assertEquals( 90, $calculator->calculate(1000) ); }
اگر روزی کسی فرمول محاسبه را اشتباه تغییر دهد، تست بلافاصله خطا را مشخص میکند.
مزایای تستنویسی فقط پیدا کردن باگ نیست.
زمانی که صدها تست دارید، با خیال راحت ریفکتور میکنید.
رفع باگ در محیط پروداکشن معمولا بسیار گرانتر از پیدا کردن آن در زمان توسعه است.
تستها نشان میدهند سیستم دقیقا چگونه باید رفتار کند.
کدی که تستپذیر باشد معمولا ساختار بهتری دارد.
ممکن است ابتدا کمی کندتر پیش بروید اما در ادامه سرعت توسعه به شکل محسوسی افزایش پیدا میکند.
PHPUnit مشهورترین فریمورک تست در دنیای PHP است.
تقریبا تمام فریمورکهای PHP از جمله لاراول روی PHPUnit بنا شدهاند.
نمونه تست در PHPUnit:
class CalculatorTest extends TestCase { public function test_sum() { $this->assertEquals( 10, 5 + 5 ); } }
این ساختار سالهاست استاندارد صنعت محسوب میشود.
Pest یک لایه مدرن روی PHPUnit است.
Pest همان قدرت PHPUnit را ارائه میدهد اما با سینتکس سادهتر و خواناتر.
مثال قبلی در Pest:
it('calculates sum correctly', function () { expect(5 + 5)->toBe(10); });
بسیاری از توسعهدهندگان لاراول امروزه Pest را به دلیل خوانایی بیشتر ترجیح میدهند.
این سوال تقریبا در تمام پروژههای جدید مطرح میشود.
مزایا:
استاندارد صنعت
مستندات فراوان
سازگاری کامل با اکوسیستم PHP
مناسب پروژههای قدیمی
معایب:
سینتکس طولانیتر
خوانایی کمتر
مزایا:
سینتکس تمیز
خوانایی بسیار بالا
مناسب BDD
سرعت توسعه بیشتر
معایب:
جدیدتر از PHPUnit
برخی تیمها هنوز ترجیح میدهند فقط PHPUnit استفاده کنند
اگر پروژه جدیدی را شروع میکنید:
Pest انتخاب بهتری است.
اگر روی پروژهای قدیمی کار میکنید که صدها تست PHPUnit دارد:
همان PHPUnit را ادامه دهید.
composer require pestphp/pest --dev
سپس:
php artisan pest:install
به طور کلی دو دسته اصلی داریم:
کوچکترین واحد برنامه را تست میکند.
مثال:
TaxCalculator PriceFormatter DiscountService
رفتار چند بخش سیستم را با هم بررسی میکند.
مثال:
ثبت نام کاربر
ورود
ثبت سفارش
ایجاد محصول
Feature Test در پروژههای لاراولی بیشترین کاربرد را دارد.
php artisan make:test TaxCalculatorTest
یا:
php artisan make:test TaxCalculatorTest --unit
کلاس:
class TaxCalculator { public function calculate(float $amount): float { return $amount * 0.09; } }
تست:
public function test_tax_is_calculated() { $calculator = new TaxCalculator(); $result = $calculator->calculate(1000); $this->assertEquals( 90, $result ); }
اجرای تست:
php artisan test
فرض کنید Route زیر وجود دارد:
Route::get('/health', function () { return response()->json([ 'status' => 'ok' ]); });
تست:
public function test_health_endpoint() { $response = $this->get('/health'); $response->assertStatus(200); $response->assertJson([ 'status' => 'ok' ]); }
فرض کنید API محصولات داریم.
$response = $this->getJson('/api/products'); $response->assertOk();
بررسی ساختار پاسخ:
$response->assertJsonStructure([ '*' => [ 'id', 'title', 'price' ] ]);
$response = $this->post('/register', [ 'name' => 'Ashkan', 'email' => 'ashkan@test.com', 'password' => 'password', 'password_confirmation' => 'password', ]);
بررسی:
$response->assertRedirect();
$user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password' ]);
$this->assertAuthenticated();
$this->actingAs($user); $this->post('/logout'); $this->assertGuest();
به جای ساخت دستی دادهها:
$user = User::factory()->create();
یا:
$users = User::factory() ->count(10) ->create();
این روش باعث تمیزتر شدن تستها میشود.
لاراول امکانات فوقالعادهای برای تست دیتابیس دارد.
ابتدا:
use RefreshDatabase;
سپس:
uses(RefreshDatabase::class);
یا:
use RefreshDatabase;
در PHPUnit:
class ExampleTest extends TestCase { use RefreshDatabase; }
$this->assertDatabaseHas( 'users', [ 'email' => 'ashkan@test.com' ] );
$this->assertDatabaseMissing( 'users', [ 'email' => 'wrong@test.com' ] );
$this->assertSoftDeleted($user);
گاهی نمیخواهیم سرویس واقعی اجرا شود.
مثلا:
سرویس پیامک
سرویس پرداخت
API خارجی
در این شرایط از Mock استفاده میکنیم.
فرض کنید:
interface SmsProvider { public function send( string $phone, string $message ); }
در تست:
$sms = Mockery::mock( SmsProvider::class ); $sms->shouldReceive('send') ->once();
لاراول ابزارهای Fake آماده دارد.
Mail::fake();
بررسی:
Mail::assertSent( WelcomeMail::class );
Queue::fake();
Queue::assertPushed( SendInvoiceJob::class );
Event::fake();
Event::assertDispatched( UserRegistered::class );
Notification::fake();
Notification::assertSentTo( $user, WelcomeNotification::class );
فرض کنید Route فقط برای کاربران احراز هویت شده قابل دسترس است.
$response = $this->get('/dashboard'); $response->assertRedirect( '/login' );
$user = User::factory()->create(); $response = $this ->actingAs($user) ->get('/dashboard'); $response->assertOk();
فرض کنید ایمیل الزامی است.
$response = $this->post( '/register', [] );
بررسی:
$response ->assertSessionHasErrors([ 'email' ]);
$this->expectException( InvalidArgumentException::class );
سپس:
$service->execute();
برای مشاهده میزان پوشش تست:
php artisan test --coverage
نمونه خروجی:
Classes: 82% Methods: 89% Lines: 91%
اما مراقب باشید.
Coverage بالا الزاما به معنی تست خوب نیست.
ممکن است ۹۵ درصد کاوریج داشته باشید اما سناریوهای مهم را تست نکرده باشید.
tests/ ├── Unit ├── Feature ├── Helpers ├── Traits └── Factories
این ساختار در پروژههای بزرگ نگهداری تستها را سادهتر میکند.
نباید Implementation را تست کنید.
رفتار را تست کنید.
هر تست باید مستقل باشد.
بد:
test1()
خوب:
test_user_can_create_order()
دادههای تست را دستی نسازید.
فقط مسیر موفقیت را تست نکنید.
سناریوهای شکست معمولا مهمتر هستند.
اولویت پیشنهادی در پروژههای واقعی:
Authentication
Authorization
Payment Logic
Business Rules
API Endpoints
Critical Jobs
Event Listeners
و در انتها:
Helperهای ساده
خیر.
مثلا برای این کد:
return strtoupper($name);
نوشتن تست معمولا ارزش چندانی ندارد.
اما برای:
calculateInvoiceTax()
یا:
approveOrder()
یا:
generateMonthlyReport()
قطعاً باید تست وجود داشته باشد.
تستنویسی در لاراول دیگر یک قابلیت لوکس یا مخصوص شرکتهای بزرگ نیست، بلکه بخشی ضروری از فرآیند توسعه نرمافزار محسوب میشود. هرچه پروژه بزرگتر شود، ارزش تستها بیشتر نمایان میشود. بدون تست، هر تغییر میتواند باعث ایجاد باگهای غیرمنتظره شود و توسعهدهنده را از ریفکتور کردن یا اضافه کردن قابلیتهای جدید بترساند.
لاراول با فراهم کردن ابزارهایی مانند PHPUnit، Pest PHP، Model Factory، Database Testing، Mocking و Fakeها، تقریبا تمام امکانات مورد نیاز برای ایجاد یک مجموعه تست حرفهای را در اختیار شما قرار میدهد.
اگر امروز هیچ تستی در پروژه خود ندارید، لازم نیست از همان ابتدا صدها تست بنویسید. از بخشهای حیاتی سیستم شروع کنید؛ ثبتنام، ورود، قوانین کسبوکار و APIها. سپس به تدریج پوشش تست را افزایش دهید. در مدت کوتاهی متوجه خواهید شد که تستنویسی نه تنها زمان شما را هدر نمیدهد، بلکه سرعت توسعه، کیفیت کد و اعتماد به تغییرات را به شکل محسوسی افزایش میدهد.