چرا برای پروژهی لاراول تست بنویسیم؟
تست خودکار قبل از هر چیز یک شبکهی ایمنی است: وقتی قانون تخفیف را عوض میکنید یا یک ستون را تغییر نام میدهید، در چند ثانیه میفهمید چه چیزی خراب شده، نه دو هفته بعد از گزارش مشتری. در پروژههایی که چند نفر روی آن کار میکنند، تستها عملاً مستندات اجرایی رفتار سیستم هستند.
لازم نیست پوشش ۱۰۰ درصدی هدف باشد. از مسیرهایی شروع کنید که خرابیشان پول یا اعتماد را از بین میبرد: ثبتنام و ورود، پرداخت، سطح دسترسیها و هر API که کلاینتهای دیگر به آن وابستهاند. اگر احراز هویت با JWT دارید، تست «کاربر بدون توکن چه میبیند» باید از روز اول وجود داشته باشد.
PHPUnit یا Pest؟
لاراول هر دو را پشتیبانی میکند و هر دو در پشت صحنه از PHPUnit استفاده میکنند. تفاوت در ظاهر کد و تجربهی توسعهدهنده است:
| معیار | PHPUnit | Pest |
|---|---|---|
| سبک نوشتن | کلاس و متد (test_...) | تابعهای it() و test() |
| میزان کد تکراری | بیشتر | کمتر |
| افزونهها و آشنایی تیم | استاندارد قدیمی و آشنا برای اغلب تیمها | نیازمند یادگیری یک API کوچک |
| مناسب برای | پروژههای موجود و تیمهای بزرگ | پروژههای جدید و تیمهایی که خوانایی را ترجیح میدهند |
توصیهی ما: اگر پروژه از قبل PHPUnit دارد، آن را عوض نکنید؛ هر دو سبک میتوانند در یک پروژه کنار هم اجرا شوند. اجرای تستها هم یکسان است:
# run everything
php artisan test
# run in parallel (needs the brianium/paratest package)
php artisan test --parallel
# run a single test by name
php artisan test --filter=test_author_can_publish_postتست Feature در برابر تست Unit
در لاراول دو پوشهی tests/Feature و tests/Unit داریم و مرز آنها ساده است: تست Unit فقط یک کلاس یا تابع را بدون فریمورک و پایگاهداده بررسی میکند؛ تست Feature بخشی از اپلیکیشن را با هم اجرا میکند، معمولاً از یک درخواست HTTP تا نوشتن در دیتابیس.
چه چیزی Unit باشد؟
منطق خالص: محاسبهی قیمت، قوانین تخفیف، تبدیل فرمتها، اعتبارسنجیهای سفارشی. این تستها در میلیثانیه اجرا میشوند چون به هیچ چیز بیرونی وصل نیستند.
<?php
use PHPUnit\Framework\TestCase;
use App\Services\PriceCalculator;
class PriceCalculatorTest extends TestCase
{
public function test_discount_is_applied_before_tax(): void
{
$calc = new PriceCalculator(taxRate: 0.09);
// 1000 - 10% = 900, then 9% tax = 981
$this->assertSame(981, $calc->total(1000, discountPercent: 10));
}
}چه چیزی Feature باشد؟
هر چیزی که به مسیرها، میانافزارها، اعتبارسنجی، مدلها و دیتابیس وابسته است؛ یعنی بیشتر کد یک API معمولی. از آنجا که باگها معمولاً در اتصال قطعهها رخ میدهند و نه داخل یک تابع ساده، بخش بزرگی از اطمینان شما از همین تستها میآید.

فکتوری و RefreshDatabase
هر تست باید از وضعیتی معلوم شروع کند و به نتیجهی تست قبلی وابسته نباشد. تریت RefreshDatabase این کار را میکند: مهاجرتها (migrations) را یک بار اجرا میکند و هر تست را داخل یک تراکنش قرار میدهد که در پایان برگردانده میشود. فکتوریها هم دادهی نمونه را میسازند:
<?php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(),
'body' => fake()->paragraph(),
'published_at' => null,
];
}
// a named state keeps tests readable
public function published(): static
{
return $this->state(fn () => ['published_at' => now()]);
}
}در تست مینویسید Post::factory()->published()->count(3)->create() و بهجای شش خط دادهی دستی، قصد تست روشن میماند. برای سرعت، در phpunit.xml معمولاً از SQLite درونحافظه استفاده میشود؛ فقط اگر کوئریهای خاص MySQL یا PostgreSQL دارید، تستها را روی همان موتور اجرا کنید تا تفاوت رفتار شما را غافلگیر نکند.
تست درخواستهای HTTP
متدهای get، postJson و همخانوادهها درخواست را بدون سرور واقعی به اپلیکیشن میفرستند و روی پاسخ و دیتابیس ادعا (assertion) مینویسند. یک تست کامل برای ساخت پست:
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CreatePostTest extends TestCase
{
use RefreshDatabase;
public function test_author_can_publish_post(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', ['title' => 'Hello', 'body' => 'World'])
->assertCreated()
->assertJsonPath('data.title', 'Hello');
$this->assertDatabaseHas('posts', ['title' => 'Hello', 'user_id' => $user->id]);
}
public function test_title_is_required(): void
{
$this->actingAs(User::factory()->create())
->postJson('/api/posts', ['body' => 'No title'])
->assertUnprocessable()
->assertJsonValidationErrors(['title']);
}
}همین تست با Pest کوتاهتر است:
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('requires a title', function () {
$this->actingAs(User::factory()->create())
->postJson('/api/posts', ['body' => 'No title'])
->assertUnprocessable()
->assertJsonValidationErrors(['title']);
});برای هر endpoint حداقل سه حالت را بپوشانید: مسیر موفق، ورودی نامعتبر و کاربر بدون اجازه (۴۰۱ یا ۴۰۳). این سه حالت بیشتر باگهای واقعی API را میگیرند؛ اصول طراحیشان را در طراحی API های مقیاسپذیر بخوانید.
جایگزینکردن وابستگیهای بیرونی
تستی که ایمیل واقعی میفرستد، به API بانک وصل میشود یا کار را در صف واقعی میگذارد، کند و ناپایدار است. لاراول برای این کار fake آماده دارد:
use Illuminate\Support\Facades\{Mail, Http, Queue};
Mail::fake();
Queue::fake();
Http::fake([
'api.payment.test/*' => Http::response(['status' => 'paid'], 200),
]);
// ... run the code under test ...
Mail::assertSent(WelcomeMail::class);
Queue::assertPushed(SendInvoice::class);
Http::assertSent(fn ($request) => $request->url() === 'https://api.payment.test/charge');اگر کار سنگین را به صف میسپارید، تست کنید که Job درست در صف قرار گرفته و Job را جداگانه با دادهی نمونه اجرا کنید. کش و Redis را هم در تست یا با درایور array جایگزین کنید یا طبق راهنمای کش با Redis یک نمونهی جدا برای تست بالا بیاورید.
چکلیست یک مجموعهی تست سالم
- هر تست فقط یک رفتار را بررسی میکند و نامش آن رفتار را میگوید.
- تستها مستقل هستند و با هر ترتیبی (و بهصورت موازی) میتوانند اجرا شوند.
- هیچ تستی به اینترنت، ساعت واقعی یا سرویس بیرونی وابسته نیست؛ برای زمان از
$this->travelTo()استفاده کنید. - تستها در هر push روی CI اجرا میشوند و شکست آنها ادغام را متوقف میکند. برای ساخت محیط یکسان، دیپلوی با Docker را ببینید.
- برای هر باگ تولیدی، قبل از رفع، یک تست شکستخورده بنویسید تا دوباره تکرار نشود.
مستندات رسمی لاراول بخش Testing را با همهی ادعاها و fakeها فهرست کرده است. اگر برای پروژهی خود به راهاندازی تست و CI کمک میخواهید، خدمات توسعهی وب ما را ببینید.
پرسشهای متداول
تست Feature و Unit در لاراول چه فرقی دارند؟
تست Unit یک کلاس یا تابع را جدا از فریمورک و دیتابیس میسنجد و بسیار سریع است. تست Feature چند بخش اپلیکیشن را با هم اجرا میکند، مثلاً درخواست HTTP، اعتبارسنجی و ذخیره در دیتابیس، و رفتار واقعی را بهتر نشان میدهد.
RefreshDatabase چه کاری میکند؟
این تریت مهاجرتها را اجرا میکند تا ساختار دیتابیس آماده باشد و هر تست را در یک تراکنش میگذارد که در پایان برگردانده میشود. نتیجه این است که هر تست با دیتابیس تمیز شروع میشود و دادهی تستهای دیگر دیده نمیشود.
برای پروژهی جدید لاراول Pest بهتر است یا PHPUnit؟
هر دو روی یک موتور اجرا میشوند، پس تفاوت اصلی در خوانایی است. Pest کد تکراری کمتری دارد و برای پروژهی جدید مناسب است؛ اگر تیم PHPUnit را میشناسد یا پروژه از قبل تست دارد، ماندن روی آن کاملاً منطقی است.
تستهای لاراول چقدر باید پوشش داشته باشند؟
عدد ثابتی وجود ندارد. اول مسیرهای حیاتی مانند ورود، پرداخت و دسترسیها را کامل بپوشانید، سپس با هر باگ جدید تست اضافه کنید. پوشش ۱۰۰ درصدی معمولاً هزینهای بیش از ارزش خود دارد.

دیدگاهها
هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.