📖 درس نهم: تست و دیباگ API حرفهای
🎯 هدف این درس: آشنایی با تستنویسی حرفهای API با PHPUnit، Mocking، Logging پیشرفته، استفاده از Postman Collection و تکنیکهای دیباگ و عیبیابی.
مهمان عزیز! 👋
تا اینجا یک API کامل با امنیت، احراز هویت، مستندات، کش و نسخهبندی ساختهایم. اما چطور مطمئن شویم که API ما درست کار میکند؟
پاسخ: تستنویسی! در این درس با ابزارها و تکنیکهای تست و دیباگ حرفهای آشنا میشویم تا بتوانیم API خود را به صورت کامل تست کنیم و از صحت عملکرد آن اطمینان حاصل کنیم.
💡 چرا تستنویسی مهم است؟
- ✅ اطمینان از صحت عملکرد API.
- ✅ جلوگیری از بازگشت باگها (Regression).
- ✅ مستندسازی خودکار رفتار API.
- ✅ افزایش اعتماد به کد.
- ✅ تسهیل همکاری تیمی.
- ✅ کاهش هزینههای نگهداری.
🧪 نصب و راهاندازی PHPUnit
PHPUnit محبوبترین فریمورک تستنویسی در PHP است. برای نصب آن از Composer استفاده میکنیم:
Install PHPUnit
Composer
# نصب PHPUnit به عنوان وابستگی توسعه
composer require --dev phpunit/phpunit
# اجرای تستها
vendor/bin/phpunit
# فایل phpunit.xml (پیکربندی)
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
verbose="true"
stopOnFailure="false">
<testsuites>
<testsuite name="API Tests">
<directory>tests/Unit</directory>
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
</phpunit>
🧪 نوشتن Unit Test برای Model
ابتدا تستهای واحد (Unit Test) برای مدل User مینویسیم:
UserTest.php
PHP
8.2
<?php
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
private $userModel;
private $db;
// قبل از هر تست، دیتابیس تست را آماده کن
protected function setUp(): void
{
parent::setUp();
// اتصال به دیتابیس تست (SQLite in-memory)
$this->db = new PDO('sqlite::memory:');
$this->db->exec("CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// تزریق دیتابیس به مدل (با Reflection)
$this->userModel = new User();
$reflection = new ReflectionClass($this->userModel);
$property = $reflection->getProperty('db');
$property->setAccessible(true);
$property->setValue($this->userModel, $this->db);
}
// تست ایجاد کاربر
public function testCreateUser()
{
$userData = [
'name' => 'یونس',
'email' => 'younes@example.com',
'password' => 'password123',
'role' => 'admin'
];
$user = $this->userModel->create($userData);
// Assertions
$this->assertNotNull($user);
$this->assertEquals('یونس', $user['name']);
$this->assertEquals('younes@example.com', $user['email']);
$this->assertEquals('admin', $user['role']);
$this->assertArrayHasKey('id', $user);
}
// تست پیدا کردن کاربر
public function testFindUser()
{
// ایجاد کاربر
$userData = [
'name' => 'مریم',
'email' => 'maryam@example.com',
'password' => 'password123'
];
$created = $this->userModel->create($userData);
// پیدا کردن کاربر
$found = $this->userModel->find($created['id']);
$this->assertNotNull($found);
$this->assertEquals('مریم', $found['name']);
$this->assertEquals('maryam@example.com', $found['email']);
}
// تست بروزرسانی کاربر
public function testUpdateUser()
{
// ایجاد کاربر
$userData = [
'name' => 'علی',
'email' => 'ali@example.com',
'password' => 'password123'
];
$created = $this->userModel->create($userData);
// بروزرسانی
$updated = $this->userModel->update($created['id'], [
'name' => 'علی رضا',
'role' => 'admin'
]);
$this->assertNotNull($updated);
$this->assertEquals('علی رضا', $updated['name']);
$this->assertEquals('admin', $updated['role']);
}
// تست حذف کاربر
public function testDeleteUser()
{
// ایجاد کاربر
$userData = [
'name' => 'سارا',
'email' => 'sara@example.com',
'password' => 'password123'
];
$created = $this->userModel->create($userData);
// حذف کاربر
$deleted = $this->userModel->delete($created['id']);
$this->assertTrue($deleted);
// بررسی اینکه کاربر دیگر وجود ندارد
$found = $this->userModel->find($created['id']);
$this->assertNull($found);
}
// تست ایمیل تکراری
public function testEmailExists()
{
// ایجاد کاربر
$userData = [
'name' => 'تست',
'email' => 'test@example.com',
'password' => 'password123'
];
$this->userModel->create($userData);
// بررسی وجود ایمیل
$exists = $this->userModel->emailExists('test@example.com');
$this->assertTrue($exists);
$notExists = $this->userModel->emailExists('nonexistent@example.com');
$this->assertFalse($notExists);
}
}
🧪 تست Endpoint (Feature Test)
تست ویژگی (Feature Test) برای Endpointهای کامل API:
UserApiTest.php
PHP
8.2
<?php
use PHPUnit\Framework\TestCase;
class UserApiTest extends TestCase
{
private $baseUrl = 'http://localhost:8000';
private $authToken;
// قبل از هر تست، توکن دریافت کن
protected function setUp(): void
{
parent::setUp();
// ثبتنام یا ورود برای دریافت توکن
$response = $this->sendRequest('POST', '/auth/login', [
'email' => 'admin@example.com',
'password' => 'admin123'
]);
if (isset($response['data']['token'])) {
$this->authToken = $response['data']['token'];
}
}
// متد کمکی برای ارسال درخواست
private function sendRequest($method, $endpoint, $data = [])
{
$url = $this->baseUrl . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
'Authorization: Bearer ' . $this->authToken
]);
if (!empty($data)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status_code' => $httpCode,
'body' => json_decode($response, true)
];
}
// تست دریافت لیست کاربران
public function testGetUsers()
{
$response = $this->sendRequest('GET', '/v2/users');
$this->assertEquals(200, $response['status_code']);
$this->assertEquals('success', $response['body']['status']);
$this->assertArrayHasKey('data', $response['body']);
$this->assertArrayHasKey('pagination', $response['body']['data']);
}
// تست ایجاد کاربر
public function testCreateUser()
{
$userData = [
'name' => 'کاربر تست',
'email' => 'testuser@example.com',
'password' => 'password123',
'role' => 'user'
];
$response = $this->sendRequest('POST', '/v2/users', $userData);
$this->assertEquals(201, $response['status_code']);
$this->assertEquals('success', $response['body']['status']);
$this->assertEquals('کاربر تست', $response['body']['data']['name']);
}
// تست دریافت یک کاربر
public function testGetUser()
{
// ابتدا یک کاربر ایجاد کن
$userData = [
'name' => 'کاربر جستجو',
'email' => 'search@example.com',
'password' => 'password123'
];
$createResponse = $this->sendRequest('POST', '/v2/users', $userData);
$userId = $createResponse['body']['data']['id'];
// دریافت کاربر
$response = $this->sendRequest('GET', '/v2/users/' . $userId);
$this->assertEquals(200, $response['status_code']);
$this->assertEquals('کاربر جستجو', $response['body']['data']['name']);
}
// تست اعتبارسنجی
public function testValidationErrors()
{
$invalidData = [
'name' => '',
'email' => 'invalid-email',
'password' => '123'
];
$response = $this->sendRequest('POST', '/v2/users', $invalidData);
$this->assertEquals(422, $response['status_code']);
$this->assertEquals('error', $response['body']['status']);
$this->assertArrayHasKey('errors', $response['body']);
}
// تست حذف کاربر
public function testDeleteUser()
{
// ایجاد کاربر
$userData = [
'name' => 'کاربر حذف',
'email' => 'delete@example.com',
'password' => 'password123'
];
$createResponse = $this->sendRequest('POST', '/v2/users', $userData);
$userId = $createResponse['body']['data']['id'];
// حذف کاربر
$response = $this->sendRequest('DELETE', '/v2/users/' . $userId);
$this->assertEquals(204, $response['status_code']);
}
}
🎭 Mocking در تست
Mocking به شما امکان میدهد وابستگیها را شبیهسازی کنید و تستهای ایزوله بنویسید:
Mocking Example
PHP
8.2
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
class UserControllerTest extends TestCase
{
/**
* @var User|MockObject
*/
private $userModelMock;
private $controller;
protected function setUp(): void
{
parent::setUp();
// ایجاد Mock برای User Model
$this->userModelMock = $this->createMock(User::class);
// ایجاد کنترلر با Mock
$this->controller = new UserController();
// تزریق Mock به کنترلر (با Reflection)
$reflection = new ReflectionClass($this->controller);
$property = $reflection->getProperty('userModel');
$property->setAccessible(true);
$property->setValue($this->controller, $this->userModelMock);
}
// تست دریافت کاربر با Mock
public function testShowUserWithMock()
{
// دادههای مورد انتظار
$expectedUser = [
'id' => 1,
'name' => 'یونس',
'email' => 'younes@example.com'
];
// تنظیم Mock برای متد find
$this->userModelMock
->expects($this->once())
->method('find')
->with(1)
->willReturn($expectedUser);
// اجرای متد
$request = new Request();
$response = $this->controller->show($request, [1]);
// Assertions
$this->assertEquals(200, $response->statusCode);
$this->assertEquals('یونس', $response->body['data']['name']);
}
// تست خطای پیدا نشدن کاربر
public function testShowUserNotFound()
{
// Mock برای بازگشت null
$this->userModelMock
->expects($this->once())
->method('find')
->with(999)
->willReturn(null);
$request = new Request();
$response = $this->controller->show($request, [999]);
$this->assertEquals(404, $response->statusCode);
$this->assertEquals('User not found', $response->body['message']);
}
// تست ایجاد کاربر با Mock
public function testStoreUserWithMock()
{
$userData = [
'name' => 'کاربر جدید',
'email' => 'new@example.com',
'password' => 'password123'
];
$expectedUser = $userData + ['id' => 10];
// Mock برای متد create
$this->userModelMock
->expects($this->once())
->method('create')
->with($userData)
->willReturn($expectedUser);
// Mock برای emailExists
$this->userModelMock
->expects($this->once())
->method('emailExists')
->willReturn(false);
$request = new Request();
$request->body = $userData;
$response = $this->controller->store($request);
$this->assertEquals(201, $response->statusCode);
$this->assertEquals('کاربر جدید', $response->body['data']['name']);
}
}
📝 Logging پیشرفته با Monolog
Monolog محبوبترین کتابخانه لاگگیری در PHP است:
Monolog Setup
PHP
8.2
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\FirePHPHandler;
class AppLogger
{
private static $instance = null;
private $logger;
private function __construct()
{
$this->logger = new Logger('api');
// لاگ در فایل با چرخش روزانه
$logFile = __DIR__ . '/../storage/logs/api.log';
$rotatingHandler = new RotatingFileHandler($logFile, 30, Logger::INFO);
$this->logger->pushHandler($rotatingHandler);
// لاگ خطاها در فایل جداگانه
$errorLogFile = __DIR__ . '/../storage/logs/errors.log';
$errorHandler = new StreamHandler($errorLogFile, Logger::ERROR);
$this->logger->pushHandler($errorHandler);
// در محیط توسعه، لاگ را در FirePHP نمایش بده
if ($_ENV['APP_ENV'] === 'development') {
$this->logger->pushHandler(new FirePHPHandler());
}
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function info($message, $context = [])
{
$this->logger->info($message, $context + $this->getDefaultContext());
}
public function error($message, $context = [])
{
$this->logger->error($message, $context + $this->getDefaultContext());
}
public function warning($message, $context = [])
{
$this->logger->warning($message, $context + $this->getDefaultContext());
}
public function debug($message, $context = [])
{
if ($_ENV['APP_ENV'] === 'development') {
$this->logger->debug($message, $context + $this->getDefaultContext());
}
}
private function getDefaultContext()
{
return [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
'uri' => $_SERVER['REQUEST_URI'] ?? 'unknown',
'user_id' => $GLOBALS['auth_user']['user_id'] ?? 'guest'
];
}
}
// استفاده در کنترلر
public function store($request)
{
try {
// ... کد ایجاد کاربر ...
AppLogger::getInstance()->info('User created', ['user_id' => $user['id']]);
return Response::created($user);
} catch (Exception $e) {
AppLogger::getInstance()->error('Failed to create user', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return Response::error('Failed to create user', 500);
}
}
🔧 دیباگ با Xdebug و Postman
ابزارهای دیباگ حرفهای برای عیبیابی API:
| ابزار | کاربرد | تنظیمات |
|---|---|---|
| Xdebug | دیباگ Step-by-Step در کد |
zend_extension=xdebug.soxdebug.mode=debugxdebug.start_with_request=yes
|
| Postman | تست و دیباگ API |
Collections, Environments Console (View → Show Postman Console) |
| Whoops | نمایش خطاهای زیبا |
composer require filp/whoops
|
| VarDumper | دیباگ متغیرها |
composer require symfony/var-dumperdump($variable);
|
Debugging Tools Setup
PHP
8.2
<?php
// ========== ۱. راهاندازی Whoops ==========
require_once __DIR__ . '/../vendor/autoload.php';
use Whoops\Run;
use Whoops\Handler\PrettyPageHandler;
$whoops = new Run();
$whoops->pushHandler(new PrettyPageHandler());
// فقط در محیط توسعه فعال کن
if ($_ENV['APP_ENV'] === 'development') {
$whoops->register();
}
// ========== ۲. VarDumper برای دیباگ سریع ==========
use Symfony\Component\VarDumper\VarDumper;
function dd(...$vars)
{
foreach ($vars as $var) {
VarDumper::dump($var);
}
exit(1);
}
// استفاده در کنترلر (فقط در محیط توسعه)
public function debugExample($request)
{
if ($_ENV['APP_ENV'] === 'development') {
dd($request->getBody(), $_SERVER);
}
// ... کد اصلی ...
}
// ========== ۳. Postman Console ==========
// در Postman، Console را باز کن (View → Show Postman Console)
// تمام درخواستها و پاسخها را در Console مشاهده کن
// ========== ۴. Xdebug + VS Code ==========
// تنظیمات launch.json در VS Code:
// {
// "version": "0.2.0",
// "configurations": [
// {
// "name": "Listen for XDebug",
// "type": "php",
// "request": "launch",
// "port": 9003
// }
// ]
// }
📊 Postman Collection و تست خودکار
Postman Collection به شما امکان میدهد تستهای API را ذخیره و به اشتراک بگذارید:
Postman Collection (JSON)
JSON
// Postman Collection - بخشی از تعریف
{
"info": {
"name": "API User Management",
"version": "1.0.0"
},
"item": [
{
"name": "Login",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"email\": \"admin@example.com\",\n \"password\": \"admin123\"\n}"
},
"url": {
"raw": "{{base_url}}/auth/login",
"host": ["{{base_url}}"],
"path": ["auth", "login"]
}
},
"response": []
},
{
"name": "Get Users",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{auth_token}}"
}
],
"url": {
"raw": "{{base_url}}/v2/users?page=1&limit=10",
"host": ["{{base_url}}"],
"path": ["v2", "users"],
"query": [
{
"key": "page",
"value": "1"
},
{
"key": "limit",
"value": "10"
}
]
}
}
}
],
"variable": [
{
"key": "base_url",
"value": "http://localhost:8000"
},
{
"key": "auth_token",
"value": ""
}
]
}
🔴 خطاهای رایج در تست و دیباگ
- ❌ اشتباه: تستهایی که به ترتیب اجرا وابسته هستند.
- ✅ درست: هر تست باید مستقل باشد و به ترتیب اجرا وابسته نباشد.
- ❌ اشتباه: استفاده از دیتابیس تولید در تست.
- ✅ درست: از دیتابیس جداگانه یا SQLite In-Memory استفاده کن.
- ❌ اشتباه: لاگ کردن اطلاعات حساس مثل رمز عبور.
- ✅ درست: اطلاعات حساس را از لاگ حذف کن یا Mask کن.
- ❌ اشتباه: Xdebug را در محیط تولید فعال گذاشتن.
- ✅ درست: Xdebug فقط در محیط توسعه فعال باشد.
💎 نکات کلیدی درس
- ✅ PHPUnit ابزار اصلی تستنویسی در PHP است.
- ✅ تستها به دو دسته Unit Test و Feature Test تقسیم میشوند.
- ✅ Mocking برای شبیهسازی وابستگیها در تست استفاده میشود.
- ✅ Monolog کتابخانه استاندارد لاگگیری در PHP است.
- ✅ Xdebug ابزار قدرتمند دیباگ Step-by-Step است.
- ✅ Postman Collection تستهای API را سازماندهی و به اشتراک میگذارد.
- ✅ لاگگیری و دیباگ در محیط توسعه با محیط تولید متفاوت است.
🛠️ پروژه عملی درس نهم
مهمان عزیز، حالا تستهای کامل برای API محصولات بنویس!
مسئله:
یک مجموعه تست کامل برای API محصولات پیادهسازی کن:
- ✅ تستهای Unit برای مدل Product (CRUD).
- ✅ تستهای Feature برای Endpointهای Product.
- ✅ Mock کردن وابستگیها در تستها.
- ✅ راهاندازی Monolog برای لاگگیری.
- ✅ Postman Collection کامل برای API محصولات.
- ✅ راهاندازی Xdebug برای دیباگ.
🧪 راهحل پروژه (پاسخ)
مراحل پیادهسازی:
- 1️⃣ کلاس
ProductTestبرای تست مدل ایجاد کن. - 2️⃣ کلاس
ProductApiTestبرای تست Endpointها ایجاد کن. - 3️⃣ از Mock برای شبیهسازی دیتابیس در تستها استفاده کن.
- 4️⃣ کلاس
AppLoggerرا با Monolog راهاندازی کن. - 5️⃣ Postman Collection را برای تمام Endpointهای محصولات ایجاد کن.
- 6️⃣ Xdebug را در VS Code راهاندازی کن.
📝 تمرینهای عملی
🧪 تمرین ۱ (ساده):
تستهای Unit برای مدل Product بنویس. شامل تستهای create، find، update، delete و getAll.
🧪 تمرین ۲ (متوسط):
تستهای Feature برای API محصولات بنویس. شامل تستهای GET، POST، PUT و DELETE با اعتبارسنجی.
🧪 تمرین ۳ (چالشی):
یک سیستم تست خودکار با GitHub Actions یا GitLab CI راهاندازی کن. هر بار که کد به مخزن Push میشود، تستها به صورت خودکار اجرا شوند.
🏁 جمعبندی درس
مهمان عزیز، در این درس یاد گرفتی:
- ✅ چگونه PHPUnit را نصب و راهاندازی کنی.
- ✅ چگونه تستهای Unit و Feature بنویسی.
- ✅ چگونه از Mocking برای شبیهسازی وابستگیها استفاده کنی.
- ✅ چگونه با Monolog لاگگیری حرفهای انجام دهی.
- ✅ چگونه با Xdebug و Postman دیباگ کنی.
- ✅ چگونه Postman Collection برای تست API ایجاد کنی.
حالا API شما کاملاً تست شده و قابل اعتماد است. در درس بعدی، پروژه نهایی را با هم پیادهسازی میکنیم. آمادهای؟ 🚀
🗺️ نقشه راه دوره: شما اینجا هستید!
برای اینکه بدانی دقیقاً کجای مسیر هستی و چه درسهایی در انتظار توست، به جدول زیر نگاه کن. درس فعلی با رنگ متفاوت مشخص شده است.
| درس | عنوان درس | آنچه یاد میگیرید |
|---|---|---|
| ۱ | مفاهیم پایه API | HTTP، REST، معماری Client-Server |
| ۲ | پیادهسازی RESTful API با PHP خام | مسیریابی، متدها، پارامترها، پاسخهای JSON |
| ۳ | مدیریت دادهها و پایگاهداده | اتصال PDO، CRUD، تراکنشها، Pagination |
| ۴ | اعتبارسنجی و احراز هویت | JWT، Roles/Permissions، Middleware |
| ۵ | امنیت API پیشرفته | OWASP Top 10، SQLi، XSS، CSRF، Rate Limiting |
| ۶ | مستندسازی API با OpenAPI | Swagger، OpenAPI Specification، مستندات تعاملی |
| ۷ | Caching و افزایش عملکرد | Redis، ETag، Cache-Control، Query Optimization |
| ۸ | API Versioning و مدیریت چرخه حیات | استراتژیهای نسخهبندی، Deprecation، Backward Compatibility |
| ۹ | تست و دیباگ API حرفهای | PHPUnit، Mocking، Logging، Postman Collection |
| ۱۰ | پروژه نهایی: API فروشگاهی | پروژه کامل فروشگاه اینترنتی با تمام قابلیتها |




نظر خود را بنویسید
با ثبت نظر، به بهبود محتوای ما کمک کنید