الاختبارات و TDD
أفضل ممارسات الاختبار
أفضل ممارسات الاختبار
كتابة الاختبارات شيء، لكن كتابة اختبارات جيدة وقابلة للصيانة فن. في هذا الدرس، سنستكشف أفضل الممارسات الصناعية لتنظيم وتسمية وصيانة وتحسين مجموعات الاختبار الخاصة بك مع تجنب الأخطاء الشائعة.
تنظيم الاختبارات
مجموعة اختبارات منظمة بشكل جيد تسهل العثور على الاختبارات وفهمها وصيانتها. إليك المبادئ التنظيمية الرئيسية:
هيكل الدلائل
tests/
├── Unit/
│ ├── Models/
│ │ ├── UserTest.php
│ │ └── OrderTest.php
│ ├── Services/
│ │ ├── PaymentServiceTest.php
│ │ └── EmailServiceTest.php
│ └── Helpers/
│ └── StringHelperTest.php
├── Feature/
│ ├── Auth/
│ │ ├── LoginTest.php
│ │ └── RegistrationTest.php
│ ├── Api/
│ │ ├── ProductApiTest.php
│ │ └── OrderApiTest.php
│ └── Admin/
│ └── DashboardTest.php
├── Integration/
│ ├── PaymentGatewayTest.php
│ └── EmailProviderTest.php
└── E2E/
├── CheckoutFlowTest.php
└── UserJourneyTest.php
مبادئ التنظيم:
- عكس هيكل التطبيق: يجب أن يعكس هيكل دليل الاختبار هيكل تطبيقك
- الفصل حسب نوع الاختبار: اختبارات الوحدة والميزات والتكامل والشاملة في دلائل منفصلة
- التجميع حسب الميزة: يجب تجميع الاختبارات ذات الصلة معًا (مثل جميع اختبارات المصادقة)
- فئة واحدة لكل ملف اختبار: يجب أن يختبر كل ملف اختبار فئة أو ميزة واحدة
تنظيم فئة الاختبار
<?php
namespace Tests\Feature\Auth;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class LoginTest extends TestCase
{
use RefreshDatabase;
// 1. طرق الإعداد في الأعلى
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
}
// 2. اختبارات المسار السعيد أولاً
/** @test */
public function user_can_login_with_valid_credentials()
{
// ترتيب
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123')
]);
// فعل
$response = $this->post('/login', [
'email' => 'test@example.com',
'password' => 'password123'
]);
// تأكيد
$response->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
}
// 3. الحالات الحدية وسيناريوهات الأخطاء
/** @test */
public function user_cannot_login_with_invalid_password()
{
$response = $this->post('/login', [
'email' => $this->user->email,
'password' => 'wrong-password'
]);
$response->assertSessionHasErrors('email');
$this->assertGuest();
}
/** @test */
public function login_validation_requires_email()
{
$response = $this->post('/login', [
'password' => 'password123'
]);
$response->assertSessionHasErrors('email');
}
// 4. الطرق المساعدة في الأسفل
protected function attemptLogin(array $credentials): TestResponse
{
return $this->post('/login', $credentials);
}
}
اصطلاحات التسمية
أسماء الاختبارات الواضحة والوصفية تسهل فهم ما يتم اختباره ولماذا فشل الاختبار.
أنماط تسمية طرق الاختبار
<?php
// ❌ سيء: غير واضح ما يتم اختباره
/** @test */
public function test1() { }
/** @test */
public function it_works() { }
// ✅ جيد: نمط وصفي - [إجراء]_[سيناريو]_[نتيجة متوقعة]
/** @test */
public function user_can_update_profile_with_valid_data() { }
/** @test */
public function order_creation_fails_when_product_is_out_of_stock() { }
/** @test */
public function api_returns_401_when_token_is_expired() { }
// ✅ نمط بديل - [it/should]_[السلوك المتوقع]_[عند الحالة]
/** @test */
public function it_sends_welcome_email_when_user_registers() { }
/** @test */
public function should_calculate_discount_when_coupon_is_valid() { }
/** @test */
public function it_throws_exception_when_payment_fails() { }
نصائح التسمية:
- استخدم
snake_caseلطرق الاختبار لتحسين القراءة - ابدأ بالموضوع الذي يتم اختباره (المستخدم، الطلب، API)
- قم بتضمين الشرط أو السياق (when، with، without)
- انته بالنتيجة المتوقعة (ينجح، يفشل، يعيد، يرمي)
- لا تقلق بشأن طول اسم الطريقة - الوضوح أكثر أهمية
وصف الاختبار باستخدام تعليقات PHPUnit
<?php
/**
* @test
* @group authentication
* @group critical
*/
public function user_session_expires_after_maximum_inactivity_period()
{
// تنفيذ الاختبار
}
/**
* @test
* @group api
* @group slow
* @dataProvider invalidEmailProvider
*/
public function registration_fails_with_invalid_email_format($email)
{
// تنفيذ الاختبار
}
// مزود البيانات للاختبار أعلاه
public function invalidEmailProvider(): array
{
return [
['not-an-email'],
['missing@domain'],
['@nodomain.com'],
['spaces in@email.com'],
];
}
نمط الترتيب-الفعل-التأكيد (AAA)
قم ببناء اختباراتك باستخدام نمط AAA للحصول على أقصى قدر من الوضوح:
<?php
/** @test */
public function cart_total_calculates_correctly_with_tax()
{
// ترتيب - إعداد بيانات وشروط الاختبار
$cart = new Cart();
$product = Product::factory()->create(['price' => 100]);
$taxRate = 0.15; // ضريبة 15%
// فعل - تنفيذ الإجراء الذي يتم اختباره
$cart->addProduct($product);
$total = $cart->calculateTotal($taxRate);
// تأكيد - التحقق من النتيجة المتوقعة
$this->assertEquals(115, $total);
$this->assertCount(1, $cart->items());
}
فوائد AAA:
- القراءة: يمكن لأي شخص فهم ما يفعله الاختبار
- الصيانة: من السهل تحديد وتعديل أجزاء معينة
- التركيز: يجب أن يحتوي كل اختبار على قسم "فعل" واضح واحد
- التصحيح: التأكيدات الفاشلة تشير بوضوح إلى ما حدث خطأ
استراتيجيات صيانة الاختبارات
لا تكرر نفسك (DRY)
<?php
// ❌ سيء: كود الإعداد المكرر
class OrderTest extends TestCase
{
/** @test */
public function can_create_order()
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100]);
$order = Order::create([...]);
// التأكيدات
}
/** @test */
public function can_cancel_order()
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100]);
$order = Order::create([...]);
// التأكيدات
}
}
// ✅ جيد: استخراج الإعداد المشترك
class OrderTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Product $product;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->product = Product::factory()->create(['price' => 100]);
}
/** @test */
public function can_create_order()
{
$order = $this->createOrder();
$this->assertDatabaseHas('orders', [
'user_id' => $this->user->id,
'product_id' => $this->product->id
]);
}
/** @test */
public function can_cancel_order()
{
$order = $this->createOrder();
$order->cancel();
$this->assertEquals('cancelled', $order->fresh()->status);
}
protected function createOrder(array $attributes = []): Order
{
return Order::factory()->create(array_merge([
'user_id' => $this->user->id,
'product_id' => $this->product->id
], $attributes));
}
}
تركيبات الاختبار والمصانع
<?php
// إنشاء تركيبات اختبار قابلة لإعادة الاستخدام
namespace Tests\Fixtures;
class OrderFixtures
{
public static function validOrderData(): array
{
return [
'user_id' => 1,
'total' => 150.00,
'status' => 'pending',
'items' => [
['product_id' => 1, 'quantity' => 2, 'price' => 75.00]
]
];
}
public static function expiredOrder(): Order
{
return Order::factory()->create([
'created_at' => now()->subDays(31),
'status' => 'pending'
]);
}
public static function completedOrder(): Order
{
return Order::factory()->create([
'status' => 'completed',
'completed_at' => now()
]);
}
}
// الاستخدام في الاختبارات
/** @test */
public function can_process_valid_order()
{
$orderData = OrderFixtures::validOrderData();
$order = Order::create($orderData);
$this->assertTrue($order->process());
}
الأنماط المضادة الشائعة للاختبار
1. اختبار التنفيذ بدلاً من السلوك
<?php
// ❌ سيء: يختبر تفاصيل التنفيذ
/** @test */
public function user_service_calls_repository_save_method()
{
$repository = Mockery::mock(UserRepository::class);
$repository->shouldReceive('save')
->once()
->with(Mockery::type(User::class));
$service = new UserService($repository);
$service->createUser(['name' => 'John']);
}
// ✅ جيد: يختبر السلوك والنتيجة
/** @test */
public function user_service_creates_user_successfully()
{
$userData = ['name' => 'John', 'email' => 'john@example.com'];
$user = $this->userService->createUser($userData);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('John', $user->name);
$this->assertDatabaseHas('users', ['email' => 'john@example.com']);
}
2. اختبار مخاوف متعددة في اختبار واحد
<?php
// ❌ سيء: يختبر أشياء كثيرة جدًا
/** @test */
public function user_management()
{
// ينشئ المستخدم
$user = User::create(['name' => 'John']);
$this->assertDatabaseHas('users', ['name' => 'John']);
// يحدث المستخدم
$user->update(['name' => 'Jane']);
$this->assertEquals('Jane', $user->name);
// يحذف المستخدم
$user->delete();
$this->assertDatabaseMissing('users', ['id' => $user->id]);
}
// ✅ جيد: اختبارات منفصلة لكل مخاوف
/** @test */
public function can_create_user()
{
$user = User::create(['name' => 'John']);
$this->assertDatabaseHas('users', ['name' => 'John']);
}
/** @test */
public function can_update_user_name()
{
$user = User::factory()->create(['name' => 'John']);
$user->update(['name' => 'Jane']);
$this->assertEquals('Jane', $user->fresh()->name);
}
/** @test */
public function can_delete_user()
{
$user = User::factory()->create();
$user->delete();
$this->assertDatabaseMissing('users', ['id' => $user->id]);
}
3. الاختبارات الهشة (الإفراط في التقليد)
<?php
// ❌ سيء: الكثير من التقليد يجعل الاختبارات هشة
/** @test */
public function processes_order()
{
$paymentGateway = Mockery::mock(PaymentGateway::class);
$emailService = Mockery::mock(EmailService::class);
$inventoryService = Mockery::mock(InventoryService::class);
$paymentGateway->shouldReceive('charge')
->once()
->with(100, 'token_123')
->andReturn(true);
$inventoryService->shouldReceive('decrementStock')
->once()
->with(1, 2);
$emailService->shouldReceive('send')
->once()
->with(Mockery::type(OrderConfirmation::class));
$processor = new OrderProcessor(
$paymentGateway,
$emailService,
$inventoryService
);
$processor->process($order);
}
// ✅ جيد: اختبار السلوك مع الحد الأدنى من التقليد
/** @test */
public function successfully_processes_paid_order()
{
$order = Order::factory()->create([
'status' => 'pending',
'total' => 100
]);
$result = $this->orderProcessor->process($order);
$this->assertTrue($result);
$this->assertEquals('completed', $order->fresh()->status);
$this->assertDatabaseHas('payments', [
'order_id' => $order->id,
'status' => 'paid'
]);
}
4. الاختبارات البطيئة
<?php
// ❌ سيء: تفاعلات قاعدة بيانات غير ضرورية
/** @test */
public function calculates_discount_correctly()
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100]);
$order = Order::factory()->create();
$discount = $this->calculator->calculateDiscount(100, 0.10);
$this->assertEquals(10, $discount);
}
// ✅ جيد: اختبار وحدة نقي بدون قاعدة بيانات
/** @test */
public function calculates_discount_correctly()
{
$discount = $this->calculator->calculateDiscount(100, 0.10);
$this->assertEquals(10, $discount);
}
توثيق الاختبار
<?php
/**
* @test
* @group critical
* @group payment
*
* اختبار يتحقق من أن تدفق معالجة الدفع يتعامل
* مع المعاملات المرفوضة بشكل صحيح من خلال:
* 1. محاولة شحن البطاقة
* 2. التقاط حالة الرفض
* 3. تحديث حالة الطلب
* 4. إرسال إشعار إلى المستخدم
* 5. تسجيل الفشل للتحليلات
*
* هذا حاسم لتجربة المستخدم ومنع الاحتيال.
*/
public function payment_declined_transactions_are_handled_gracefully()
{
// تنفيذ الاختبار مع تعليقات واضحة
}
تمرين تطبيقي:
أعد تصميم هذا الاختبار المكتوب بشكل سيء لاتباع أفضل الممارسات:
<?php
public function test1()
{
$u = new User();
$u->name = 'Test';
$u->email = 'test@test.com';
$u->save();
$this->assertTrue($u->exists);
$u->name = 'Updated';
$u->save();
$this->assertEquals('Updated', User::find($u->id)->name);
}
الإصلاح: استخدم التسمية المناسبة، نمط AAA، افصل المخاوف، استخدم المصانع، وأضف تأكيدات ذات معنى.
الخلاصة
اتباع أفضل ممارسات الاختبار يضمن:
- قابلية الصيانة: الاختبارات سهلة الفهم والتعديل
- الموثوقية: تتحقق الاختبارات بدقة من السلوك، وليس التنفيذ
- السرعة: مجموعة الاختبارات تعمل بسرعة، مما يشجع على التنفيذ المتكرر
- الوضوح: الاختبارات الفاشلة تشير بوضوح إلى ما حدث خطأ
- القيمة: توفر الاختبارات الثقة دون أن تصبح عبئًا
القواعد الذهبية:
- تأكيد منطقي واحد لكل اختبار (اختبار سلوك واحد)
- يجب أن تكون الاختبارات مستقلة ومعزولة
- الاختبارات السريعة يتم تشغيلها في كثير من الأحيان - حسّن للسرعة
- اختبر السلوك، وليس التنفيذ
- التسمية الواضحة أكثر أهمية من الإيجاز
- اكتب اختبارات ترغب في صيانتها بعد 6 أشهر