Unit Test در Laravel چیست؟
Unit Test یا تست واحد، روشی برای بررسی صحت عملکرد بخش کوچکی از کد است. این بخش میتواند یک متد، یک کلاس یا یک واحد مستقل از منطق برنامه باشد.
فرض کنید در یک پروژه فروشگاهی با Laravel، کلاسی برای محاسبه تخفیف محصولات نوشتهاید. این کلاس باید قیمت نهایی را بر اساس قیمت اولیه و درصد تخفیف محاسبه کند.
اگر این منطق اشتباه باشد، ممکن است قیمت محصولات در بخشهای مختلف فروشگاه نادرست نمایش داده شود. بهجای اینکه هر بار از طریق صفحات فروشگاه نتیجه را بررسی کنید، میتوانید یک تست بنویسید که مستقیماً خروجی کلاس را بررسی کند.
در واقع، Unit Test به این سؤال پاسخ میدهد:
آیا این بخش مشخص از کد، برای ورودیهای مختلف، خروجی مورد انتظار را تولید میکند؟
در Laravel میتوان برای تست واحد از PHPUnit یا Pest استفاده کرد. در این مقاله از PHPUnit استفاده میکنیم تا با ساختار کلاسمحور تستنویسی آشنا شوید.
تفاوت Unit Test و Feature Test
پیش از شروع کدنویسی، لازم است تفاوت این دو نوع تست را بدانید.
| ویژگی | Unit Test | Feature Test |
|---|---|---|
| هدف اصلی | بررسی منطق یک واحد مستقل | بررسی رفتار یک قابلیت |
| راهاندازی کامل Laravel | معمولاً خیر | معمولاً بله |
| نیاز به درخواست HTTP | معمولاً ندارد | میتواند داشته باشد |
| دسترسی به دیتابیس | در تست واحد مستقل معمولاً ندارد | امکانپذیر است |
| مثال | محاسبه مبلغ تخفیف | ثبت سفارش از طریق API |
برای مثال، اگر بخواهید مطمئن شوید یک متد مبلغ تخفیف را درست محاسبه میکند، Unit Test انتخاب مناسبی است.
اما اگر بخواهید بررسی کنید کاربر با ارسال یک درخواست HTTP میتواند سفارش ثبت کند و اطلاعات آن در دیتابیس ذخیره میشود، معمولاً باید از Feature Test استفاده کنید.
این دو روش جایگزین یکدیگر نیستند؛ هرکدام برای بررسی نوع متفاوتی از رفتار برنامه کاربرد دارند.
چرا Unit Test بنویسیم؟
استفاده از تست واحد در پروژههای Laravel چند مزیت مهم دارد.
۱. شناسایی سریع خطاها
اگر منطق یک متد را تغییر دهید و نتیجه محاسبات اشتباه شود، تست میتواند این مشکل را پیش از انتشار تغییرات شناسایی کند.
۲. سادهتر شدن تغییرات کد
وقتی کلاسها تستهای مشخصی دارند، میتوانید ساختار داخلی آنها را اصلاح کنید و دوباره بررسی کنید که رفتار مورد انتظار حفظ شده است.
۳. کاهش وابستگی به محیط اجرا
یک Unit Test مناسب معمولاً برای اجرای خود به سرور وب، دیتابیس یا سرویس خارجی نیاز ندارد. در نتیجه، اجرای آن سادهتر و معمولاً سریعتر است.
۴. مستندسازی رفتار کلاسها
نام تستها و ورودی و خروجی آنها نشان میدهند که یک کلاس قرار است چگونه رفتار کند. این موضوع برای توسعهدهندگان دیگر نیز مفید است.
۵. بررسی شرایط مرزی
تست واحد به شما کمک میکند ورودیهایی مانند صفر، مقادیر منفی، درصد تخفیف نامعتبر یا مقادیر بسیار بزرگ را بررسی کنید.
پیشنیازهای نوشتن Unit Test
برای دنبال کردن مثالهای این مقاله، به یک پروژه Laravel نیاز دارید که وابستگیهای آن نصب شده باشند.
همچنین بهتر است با مفاهیم زیر آشنا باشید:
- کلاس و متد در PHP
- نوع داده و مقدار بازگشتی متد
- فضای نام یا Namespace
- اجرای دستورات Artisan
- مفاهیم اولیه PHPUnit
مثالهای این مقاله بر اساس ساختار Laravel 13 نوشته شدهاند. در نسخههای جدید قبلی Laravel نیز ساختار کلی ایجاد و اجرای تستها مشابه است.
مرحله اول: ایجاد یک Unit Test
ترمینال را در ریشه پروژه Laravel باز کنید؛ همان مسیری که فایل artisan در آن قرار دارد.
برای ساخت یک تست واحد دستور زیر را اجرا کنید:
php artisan make:test DiscountCalculatorTest --unit
Laravel فایلی مشابه مسیر زیر ایجاد میکند:
tests/Unit/DiscountCalculatorTest.php
گزینه --unit مشخص میکند که فایل تست در پوشه tests/Unit ساخته شود.
اگر این گزینه را حذف کنید، دستور make:test بهصورت پیشفرض یک تست Feature ایجاد میکند.
مرحله دوم: ساخت کلاس مورد آزمایش
فرض کنید میخواهیم کلاسی بسازیم که مبلغ تخفیف را محاسبه کند.
در مسیر زیر یک فایل ایجاد کنید:
app/Services/DiscountCalculator.php
سپس کد زیر را داخل آن قرار دهید:
<?php
namespace App\Services;
class DiscountCalculator
{
public function calculate(
float $price,
float $discountPercent
): float {
return $price - (
$price * $discountPercent / 100
);
}
}
این کلاس یک متد به نام calculate() دارد.
متد دو ورودی دریافت میکند:
price: قیمت اولیه محصولdiscountPercent: درصد تخفیف
سپس مبلغ تخفیف را محاسبه میکند و از قیمت اولیه کم میکند.
برای مثال، اگر قیمت محصول ۱۰۰۰ و درصد تخفیف ۲۰ باشد، خروجی برابر با ۸۰۰ خواهد بود.
در این مرحله هنوز تستی ننوشتهایم؛ فقط منطق مورد آزمایش را پیادهسازی کردهایم.
مرحله سوم: نوشتن اولین Unit Test
اکنون فایل زیر را باز کنید:
tests/Unit/DiscountCalculatorTest.php
محتوای آن را با کد زیر جایگزین کنید:
<?php
namespace Tests\Unit;
use App\Services\DiscountCalculator;
use PHPUnit\Framework\TestCase;
class DiscountCalculatorTest extends TestCase
{
public function test_discount_is_calculated_correctly(): void
{
$calculator = new DiscountCalculator();
$result = $calculator->calculate(1000, 20);
$this->assertSame(800.0, $result);
}
}
بیایید بخشهای مختلف این تست را بررسی کنیم.
ساخت شیء کلاس
$calculator = new DiscountCalculator();
در این خط یک نمونه از کلاس DiscountCalculator ایجاد میکنیم.
اجرای متد
$result = $calculator->calculate(1000, 20);
قیمت ۱۰۰۰ و تخفیف ۲۰ درصد را به متد ارسال میکنیم.
بررسی نتیجه
$this->assertSame(800.0, $result);
این دستور بررسی میکند که خروجی متد دقیقاً برابر با عدد اعشاری 800.0 باشد.
متد assertSame() هم مقدار و هم نوع داده را بررسی میکند. از آنجا که متد calculate() در مثال ما نوع بازگشتی float دارد، انتظار داریم خروجی نیز 800.0 باشد.
نکته مهم این است که در این تست از کلاس زیر استفاده کردهایم:
use PHPUnit\Framework\TestCase;
این کلاس پایه PHPUnit است و برنامه Laravel را بهطور کامل راهاندازی نمیکند. برای همین مثال مستقل، نیازی به دیتابیس یا درخواست HTTP نداریم.
مرحله چهارم: اجرای تست واحد
برای اجرای تستها، در ترمینال دستور زیر را اجرا کنید:
php artisan test
اگر بخواهید فقط همین فایل تست را اجرا کنید، میتوانید مسیر آن را مشخص کنید:
php artisan test tests/Unit/DiscountCalculatorTest.php
همچنین در صورتی که PHPUnit در پروژه نصب شده باشد، امکان اجرای مستقیم آن وجود دارد:
./vendor/bin/phpunit tests/Unit/DiscountCalculatorTest.php
در صورت موفقیت، نتیجه تست بهصورت موفق نمایش داده میشود.
اگر تست شکست بخورد، گزارش اجرا نشان میدهد که مقدار واقعی با مقدار مورد انتظار یکسان نبوده است یا خطای دیگری رخ داده است.
مرحله پنجم: آزمایش ورودیهای مختلف
یک تست واحد خوب نباید تنها یک ورودی را بررسی کند. ممکن است متد برای قیمت ۱۰۰۰ درست عمل کند، اما در شرایط دیگر نتیجه نامناسبی تولید کند.
برای مثال، این ورودیها را در نظر بگیرید:
| قیمت اولیه | درصد تخفیف | خروجی مورد انتظار |
|---|---|---|
| ۱۰۰۰ | ۲۰ | ۸۰۰ |
| ۵۰۰ | ۱۰ | ۴۵۰ |
| ۱۰۰۰ | ۰ | ۱۰۰۰ |
| ۲۰۰ | ۵۰ | ۱۰۰ |
میتوانیم برای هر سناریو یک تست مستقل بنویسیم؛ اما وقتی منطق یکسان است و فقط دادهها تغییر میکنند، استفاده از Data Provider روش مناسبتری است.
مرحله ششم: استفاده از Data Provider در PHPUnit
Data Provider به ما اجازه میدهد یک متد تست را با چند مجموعه ورودی و خروجی اجرا کنیم.
فایل DiscountCalculatorTest.php را به شکل زیر تغییر دهید:
<?php
namespace Tests\Unit;
use App\Services\DiscountCalculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class DiscountCalculatorTest extends TestCase
{
#[DataProvider('discountCases')]
public function test_discount_is_calculated_correctly(
float $price,
float $discountPercent,
float $expected
): void {
$calculator = new DiscountCalculator();
$result = $calculator->calculate(
$price,
$discountPercent
);
$this->assertSame($expected, $result);
}
public static function discountCases(): array
{
return [
'twenty percent discount' => [
1000.0,
20.0,
800.0,
],
'ten percent discount' => [
500.0,
10.0,
450.0,
],
'zero discount' => [
1000.0,
0.0,
1000.0,
],
'fifty percent discount' => [
200.0,
50.0,
100.0,
],
];
}
}
در این مثال از ویژگی #[DataProvider('discountCases')] استفاده کردهایم تا PHPUnit بداند دادههای تست را از متد discountCases() دریافت کند.
هر مجموعه داده شامل سه مقدار است:
- قیمت اولیه
- درصد تخفیف
- خروجی مورد انتظار
با این روش، همان منطق با چند ورودی مختلف بررسی میشود و نیازی نیست برای هر سناریو یک متد تست جداگانه بنویسیم.
برای اجرای تست، همان دستور قبلی کافی است:
php artisan test tests/Unit/DiscountCalculatorTest.php
نکته: استفاده از #[DataProvider] به نسخه سازگار PHPUnit نیاز دارد. اگر پروژه شما از نسخه قدیمیتری استفاده میکند، ساختار Data Provider را مطابق مستندات همان نسخه تنظیم کنید.
مرحله هفتم: تست کردن شرایط مرزی
یکی از مزیتهای مهم Unit Test، بررسی شرایطی است که ممکن است در استفاده معمولی کمتر دیده شوند.
در مثال محاسبه تخفیف، شرایط مرزی میتوانند شامل موارد زیر باشند:
- قیمت صفر
- تخفیف صفر درصد
- تخفیف صد درصد
- درصد تخفیف منفی
- درصد تخفیف بیشتر از صد
- قیمت منفی
اما باید توجه کنید که رفتار صحیح در این شرایط به قوانین کسبوکار شما بستگی دارد.
برای مثال، در یک فروشگاه معمولی احتمالاً قیمت منفی یا تخفیف بیشتر از صد درصد معتبر نیست. بنابراین بهتر است کلاس خود را طوری طراحی کنید که ورودی نامعتبر را رد کند.
نسخه اصلاحشده کلاس را میتوان به این شکل نوشت:
<?php
namespace App\Services;
use InvalidArgumentException;
class DiscountCalculator
{
public function calculate(
float $price,
float $discountPercent
): float {
if ($price < 0) {
throw new InvalidArgumentException(
'Price cannot be negative.'
);
}
if ($discountPercent < 0 || $discountPercent > 100) {
throw new InvalidArgumentException(
'Discount must be between 0 and 100.'
);
}
return $price - (
$price * $discountPercent / 100
);
}
}
اکنون کلاس علاوه بر محاسبه تخفیف، ورودیهای نامعتبر را نیز بررسی میکند.
توجه داشته باشید که این تغییر، رفتار قبلی کلاس را گسترش میدهد. پس تستهای مربوط به ورودیهای معتبر و نامعتبر باید هر دو نوشته شوند.
مرحله هشتم: تست کردن Exception
در PHPUnit میتوانیم بررسی کنیم که یک متد در شرایط مشخص، استثنای مورد انتظار را ایجاد میکند.
برای آزمایش تخفیف نامعتبر، تست زیر را به کلاس DiscountCalculatorTest اضافه کنید:
public function test_invalid_discount_throws_exception(): void
{
$calculator = new DiscountCalculator();
$this->expectException(
\InvalidArgumentException::class
);
$calculator->calculate(1000, 150);
}
در این تست انتظار داریم ارسال تخفیف ۱۵۰ درصد باعث ایجاد InvalidArgumentException شود.
دستور expectException() به PHPUnit اعلام میکند که تست تنها زمانی مطابق انتظار عمل کرده است که استثنای مشخصشده رخ دهد.
اگر متد هیچ استثنایی ایجاد نکند یا استثنای دیگری رخ دهد، تست موفق نخواهد بود.
برای آزمایش قیمت منفی نیز میتوانید تست جداگانهای بنویسید:
public function test_negative_price_throws_exception(): void
{
$calculator = new DiscountCalculator();
$this->expectException(
\InvalidArgumentException::class
);
$calculator->calculate(-100, 10);
}
بهتر است هر تست یک رفتار مشخص را بررسی کند تا در صورت شکست، دلیل آن بهراحتی قابل تشخیص باشد.
مرحله نهم: تست کردن کلاسهایی که وابستگی دارند
در پروژههای واقعی، یک کلاس ممکن است برای انجام وظیفه خود به کلاس دیگری وابسته باشد.
برای مثال، یک سرویس سفارش ممکن است از یک سرویس ارسال پیامک استفاده کند. اگر هدف ما بررسی منطق سرویس سفارش باشد، لزوماً نمیخواهیم هنگام اجرای تست پیامک واقعی ارسال شود.
در چنین شرایطی میتوانیم از Mock استفاده کنیم.
Mock یک شیء جایگزین است که رفتار آن را در تست کنترل میکنیم.
فرض کنید Interface زیر را در پروژه داریم:
<?php
namespace App\Contracts;
interface SmsSender
{
public function send(
string $mobile,
string $message
): bool;
}
سپس سرویس سفارش به این Interface وابسته است:
<?php
namespace App\Services;
use App\Contracts\SmsSender;
class OrderNotificationService
{
public function __construct(
private SmsSender $smsSender
) {
}
public function notifyOrderCreated(
string $mobile
): bool {
return $this->smsSender->send(
$mobile,
'Order created'
);
}
}
اکنون میتوانیم در یک Unit Test مستقل، رفتار Mock را با PHPUnit کنترل کنیم:
<?php
namespace Tests\Unit;
use App\Contracts\SmsSender;
use App\Services\OrderNotificationService;
use PHPUnit\Framework\TestCase;
class OrderNotificationServiceTest extends TestCase
{
public function test_order_notification_is_sent(): void
{
$smsSender = $this->createMock(
SmsSender::class
);
$smsSender
->expects($this->once())
->method('send')
->with(
'09120000000',
'Order created'
)
->willReturn(true);
$service = new OrderNotificationService(
$smsSender
);
$result = $service->notifyOrderCreated(
'09120000000'
);
$this->assertTrue($result);
}
}
در این تست:
- با
createMock()یک نمونه جایگزین از Interface میسازیم. - با
expects($this->once())مشخص میکنیم که متدsend()باید دقیقاً یک بار فراخوانی شود. - با
with()آرگومانهای مورد انتظار را مشخص میکنیم. - با
willReturn(true)نتیجهای را که Mock باید برگرداند تعیین میکنیم. - در پایان، خروجی سرویس را بررسی میکنیم.
این تست بررسی میکند که سرویس سفارش درخواست ارسال پیامک را با پارامترهای مورد انتظار به وابستگی خود میدهد و نتیجه آن را برمیگرداند.
اما توجه کنید که این تست، ارسال واقعی پیامک یا صحت عملکرد سرویس پیامکی را بررسی نمیکند. هدف آن آزمایش منطق کلاس OrderNotificationService است.
همچنین برای این مثال مستقل از PHPUnit استفاده کردهایم و نیازی به راهاندازی کامل Laravel نداریم.
مرحله دهم: چه زمانی باید از امکانات Laravel در Unit Test استفاده کنیم؟
گاهی یک کلاس از امکانات فریمورک مانند Container، تنظیمات یا سرویسهای Laravel استفاده میکند. در چنین شرایطی، ممکن است بخواهید تست را با کلاس پایه Tests\TestCase بنویسید تا به محیط برنامه دسترسی داشته باشید.
اما باید بین دو مفهوم تفاوت قائل شوید:
- تست واحد مستقل که یک بخش کوچک از کد را بدون راهاندازی برنامه بررسی میکند.
- تستی که از امکانات Laravel استفاده میکند و ممکن است به راهاندازی فریمورک نیاز داشته باشد.
نامگذاری فایل در پوشه tests/Unit بهتنهایی تضمین نمیکند که تست از نظر رفتاری کاملاً مستقل باشد. مهم این است که هدف تست، وابستگیهای آن و محیط اجرای آن را درست انتخاب کنید.
اگر تست شما به درخواست HTTP، دیتابیس یا تعامل چند بخش برنامه وابسته است، ممکن است Feature Test انتخاب مناسبتری باشد.
خطاهای رایج در Unit Test
۱. وابسته کردن تست به دیتابیس بدون نیاز واقعی
اگر هدف شما بررسی یک فرمول محاسباتی است، نیازی نیست تست را به دیتابیس متصل کنید. وابستگیهای غیرضروری میتوانند اجرای تست را پیچیدهتر کنند.
۲. آزمایش نکردن ورودیهای مرزی
ممکن است متد برای ورودیهای معمولی درست کار کند اما برای صفر، مقادیر منفی یا مقادیر بسیار بزرگ نتیجه اشتباهی تولید کند.
۳. استفاده نادرست از Assertion
برای مثال، assertSame() نوع داده را هم بررسی میکند؛ درحالیکه assertEquals() برابری مقدار را با قواعد مقایسه PHPUnit میسنجد. Assertion مناسب را بر اساس رفتار مورد انتظار انتخاب کنید.
۴. تست کردن جزئیات داخلی بهجای رفتار کلاس
اگر تست به جزئیات پیادهسازی وابسته باشد، ممکن است با یک بازآرایی ساده شکست بخورد، حتی اگر رفتار مورد انتظار تغییری نکرده باشد.
۵. Mock کردن بیش از حد وابستگیها
Mock کردن همه کلاسها لزوماً تست را بهتر نمیکند. تنها وابستگیهایی را شبیهسازی کنید که برای هدف تست لازم است؛ برای مثال، سرویس خارجی یا وابستگیای که میخواهید رفتارش را کنترل کنید.
۶. نوشتن تستهایی که هیچ رفتار مهمی را بررسی نمیکنند
تستی که فقط ثابت میکند یک عبارت ساده همیشه درست است، برای بررسی منطق اصلی برنامه ارزش محدودی دارد. تستها باید رفتارهای واقعی و مورد انتظار را پوشش دهند.
بهترین روشها برای نوشتن Unit Test در Laravel
برای نوشتن تستهای واحد قابل نگهداری، نکات زیر را رعایت کنید:
- برای هر تست، نامی روشن و معنادار انتخاب کنید.
- هر تست را روی یک رفتار مشخص متمرکز نگه دارید.
- ورودیهای معمولی و شرایط مرزی را بررسی کنید.
- از Data Provider برای آزمایش چند مجموعه داده استفاده کنید.
- در صورت نیاز، وابستگیهای خارجی را با Mock کنترل کنید.
- از اتصال غیرضروری به دیتابیس یا سرویسهای واقعی اجتناب کنید.
- تستها را پس از تغییر منطق کلاس اجرا کنید.
- در صورت شکست تست، ابتدا تفاوت نتیجه واقعی و مورد انتظار را بررسی کنید.
پرسشهای متداول
چگونه در Laravel یک Unit Test بسازیم؟
در ریشه پروژه دستور زیر را اجرا کنید:
php artisan make:test DiscountCalculatorTest --unit
فایل تست در پوشه tests/Unit ایجاد میشود.
چگونه Unit Test را اجرا کنیم؟
برای اجرای همه تستها میتوانید از دستور زیر استفاده کنید:
php artisan test
برای اجرای یک فایل خاص، مسیر آن را نیز مشخص کنید:
php artisan test tests/Unit/DiscountCalculatorTest.php
آیا Unit Test به دیتابیس نیاز دارد؟
در تست واحد مستقل معمولاً نیازی به دیتابیس نداریم. اگر هدف بررسی منطق یک متد یا کلاس مستقل باشد، بهتر است تا حد امکان وابستگیهای غیرضروری را حذف کنیم.
PHPUnit چیست؟
PHPUnit یک چارچوب تستنویسی برای PHP است که امکان تعریف تست، بررسی نتیجه با Assertionها و اجرای مجموعه تستها را فراهم میکند.
Data Provider در PHPUnit چه کاربردی دارد؟
Data Provider امکان اجرای یک متد تست با چند مجموعه داده را فراهم میکند. این قابلیت برای بررسی یک منطق با ورودیها و خروجیهای مختلف مفید است.
Mock در Unit Test چه کاربردی دارد؟
Mock یک وابستگی را با یک شیء جایگزین میکند تا بتوانیم رفتار آن را کنترل کنیم. برای مثال، هنگام آزمایش سرویس سفارش میتوانیم ارسال پیامک را شبیهسازی کنیم تا پیامک واقعی ارسال نشود.
جمعبندی
Unit Test در Laravel روشی برای بررسی مستقل منطق کلاسها و متدهای برنامه است. با استفاده از PHPUnit میتوانید خروجی محاسبات، شرایط مرزی، استثناها و تعامل با وابستگیها را آزمایش کنید.
برای شروع، یک کلاس ساده بسازید، تست آن را در پوشه tests/Unit قرار دهید و با اجرای php artisan test نتیجه را بررسی کنید. سپس با گسترش تستها، ورودیهای مختلف، خطاهای احتمالی و وابستگیهای کلاس را نیز آزمایش کنید.
در پروژههای واقعی، ترکیب Unit Test و Feature Test کمک میکند هم منطق داخلی برنامه و هم رفتار قابلیتهای اصلی آن با اطمینان بیشتری بررسی شوند.
منابع رسمی برای مطالعه بیشتر
- مستندات تستنویسی Laravel: https://laravel.com/docs/13.x/testing
- مستندات HTTP Testing در Laravel: https://laravel.com/docs/13.x/http-tests
- مستندات PHPUnit: https://docs.phpunit.de/





