تست کد با PHPUnit
تست کد یکی از ارکان حیاتی توسعه نرمافزار پایدار و قابل اعتماد است. برای پروژههای PHP، PHPUnit ابزار استاندارد و قدرتمندی است که برای نوشتن تستهای واحد (unit tests)، تستهای یکپارچهسازی (integration tests) و تستهای کاربردی استفاده میشود. در این مقاله به صورت عملی، مفاهیم کلیدی، نمونهکد، و نکات حرفهای دربارهٔ تست کد با PHPUnit مطرح میکنیم.
نصب و راهاندازی سریع
composer require --dev phpunit/phpunit
این دستور PHPUnit را به عنوان وابستگی توسعه نصب میکند. پس از نصب، دستور php vendor/bin/phpunit برای اجرای تستها استفاده میشود. اگر از ابزارهایی مثل PHPUnit و Composer global استفاده میکنید، ممکن است مسیر متفاوت باشد.
ساختار پایه یک تست واحد
<?php
use PHPUnitFrameworkTestCase;
class Calculator {
public function add($a, $b) {
return $a + $b;
}
}
class CalculatorTest extends TestCase {
public function testAdd() {
$calc = new Calculator();
$this->assertEquals(4, $calc->add(2, 2));
}
}
در این مثال ساده، یک کلاس Calculator و یک کلاس تست CalculatorTest داریم. متد testAdd یک نمونه از Calculator میسازد و با assertEquals مقدار خروجی تابع add را بررسی میکند. نامگذاری متدهای تست با prefix «test» یا استفاده از @test نیز پذیرفتهشده است.
استفاده از setUp و tearDown
<?php
use PHPUnitFrameworkTestCase;
class UserRepositoryTest extends TestCase {
private $repo;
protected function setUp(): void {
$this->repo = new UserRepository();
// آمادهسازی دادهها یا اتصال به پایگاه داده آزمایشی
}
protected function tearDown(): void {
// پاکسازی منابع
unset($this->repo);
}
public function testFindUserById() {
$user = $this->repo->find(1);
$this->assertNotNull($user);
}
}
setUp قبل از اجرای هر تست اجرا میشود و برای آمادهسازی شرایط مشترک مفید است. tearDown پس از هر تست اجرا شده و منابع را آزاد میکند.
Data Provider برای پوشش حالات مختلف
<?php
use PHPUnitFrameworkTestCase;
class MathTest extends TestCase {
/**
* @dataProvider additionProvider
*/ public function testAdd($a, $b, $expected) {
$this->assertEquals($expected, $a + $b);
}
public function additionProvider() {
return [
[1, 1, 2],
[2, 3, 5],
[0, 5, 5],
];
}
}
Data Provider امکان اجرای یک متد تست با مجموعهای از ورودیها را میدهد که باعث خوانایی و کاهش تکرار در تستها میشود.
کِشش و استاب (Mocking & Stubbing)
<?php
use PHPUnitFrameworkTestCase;
interface MailerInterface {
public function send($to, $message);
}
class UserService {
private $mailer;
public function __construct(MailerInterface $mailer) {
$this->mailer = $mailer;
}
public function notify($user, $message) {
$this->mailer->send($user->email, $message);
}
}
class UserServiceTest extends TestCase {
public function testNotifySendsEmail() {
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with($this->equalTo('alice@example.com'), $this->stringContains('Welcome'));
$user = (object)['email' => 'alice@example.com'];
$service = new UserService($mailer);
$service->notify($user, 'Welcome Alice');
}
}
در این مثال از createMock برای شبیهسازی MailerInterface استفاده شده است. ما انتظار داریم که متد send دقیقاً یک بار با آرگومانهای مشخص فراخوانی شود. این تکنیک برای جداسازی واحد مورد تست از وابستگیهای خارجی بسیار مفید است.
جدول: برخی از Assertions پراستفاده
| Assertion | کاربرد |
|---|---|
| assertEquals | برابر بودن مقدار انتظار و خروجی |
| assertSame | برابر بودن با نوع و مقدار (strict) |
| assertTrue / assertFalse | بررسی بولین |
| assertNull / assertNotNull | بررسی تهی بودن مقدار |
| assertInstanceOf | بررسی نوع شیء |
| expectException | انتظار به وجود آمدن Exception |
تست استثنائات
<?php
use PHPUnitFrameworkTestCase;
class BankAccountTest extends TestCase {
public function testWithdrawThrowsException() {
$account = new BankAccount(50);
$this->expectException(InsufficientFundsException::class);
$account->withdraw(100);
}
}
متد expectException مشخص میکند که تست باید یک Exception خاص را پرتاب کند تا موفقیتآمیز باشد. این روش برای بررسی حالات خطا کاربردی است.
تستهای یکپارچهسازی و نکات عملی
- برای تستهای مربوط به پایگاه داده از دیتابیسهای موقت یا transactions استفاده کنید تا تستها ایزوله بمانند.
- از Fixtures یا seed مخصوص تست برای تهیه دادههای ثابت استفاده کنید.
- تستهای یکپارچهسازی را کند و سنگین ننویسید؛ آنها باید کمتر از تستهای واحد باشند و به عنوان پوشش سناریوهای واقعی استفاده شوند.
نکات حرفهای و بهترین شیوهها
- هر تست باید تنها یک رفتار را بررسی کند (single assertion principle نه به صورت سختگیرانه اما به عنوان راهنما).
- تستها باید سریع و قابل اجرا به صورت محلی باشند؛ اجرای تستها در CI باید خودکار شود.
- از نامگذاری واضح برای متدهای تست استفاده کنید: مثلاً testWithdrawThrowsExceptionWhenInsufficientFunds.
- برای پوششدهی بهتر از ترکیب unit tests و integration tests استفاده کنید و معیار پوشش کد (code coverage) را به عنوان تنها هدف قرار ندهید.
- از mockها فقط برای وابستگیهایی که خارج از کنترل واحد مورد تست هستند استفاده کنید؛ over-mocking باعث از دست رفتن اعتبار تست میشود.
مثال پیشرفته — تست Controller در یک فریمورک
<?php
use PHPUnitFrameworkTestCase;
use SymfonyComponentHttpFoundationRequest;
class ArticleControllerTest extends TestCase {
public function testCreateArticleReturns201() {
$request = Request::create('/articles', 'POST', [], [], [], [], json_encode([
'title' => 'New',
'body' => 'Content'
]));
// فرض بر این است که اپلیکیشن شما یک روش برای فراخوانی مسیرها در حالت تست دارد
$response = $this->handleRequest($request);
$this->assertEquals(201, $response->getStatusCode());
}
}
این مثال نشان میدهد چگونه میتوان درخواست HTTP را شبیهسازی کرد و پاسخ یک کنترلر را بررسی نمود. در فریمورکها معمولاً helperهایی برای ساخت کلاینت تست موجود است که اجرای این نوع تستها را ساده میکند.
نتیجهگیری
PHPUnit ابزار قدرتمندی است که با رعایت اصول طراحی تست، جداسازی وابستگیها و استفاده هدفمند از mockها و data providerها میتوان تستهای قابل اعتماد و قابل نگهداری نوشت. مهمترین نکته این است که تستها باید سریع، مستقل و خوانا باشند تا به فرایند توسعه کمک کنند، نه اینکه بار اضافهای ایجاد کنند.
با تمرین و ادغام تستها در جریان CI/CD، کیفیت کد و سرعت تحویل پروژه به شکل قابل توجهی بهبود مییابد.
آیا این مطلب برای شما مفید بود ؟



