home / blog / استراتيجيات الاختبار في Laravel: اكتب اخ...
لارافيل Feb 10, 2026

استراتيجيات الاختبار في Laravel: اكتب اختبارات تكتشف الأخطاء فعلاً

دليل اختبار عملي يتجاوز "assert true is true" — اختبارات التكامل، اختبارات الميزات، بدائل الاختبار، وهرم الاختبار المطبق على Laravel.

استراتيجيات الاختبار في Laravel: اكتب اختبارات تكتشف الأخطاء فعلاً

هرم الاختبار في Laravel

هرم الاختبار بسيط: اختبارات وحدة (Unit Tests) سريعة وكثيرة في القاعدة، اختبارات تكامل أقل في المنتصف، وعدد قليل من اختبارات شاملة (End-to-End) في القمة. لكن في Laravel، الحدود بين هذه الطبقات تتداخل -- وهذا أمر طبيعي تمامًا.

اختبارات Feature Tests في Laravel (التي تضرب مساراتك بطلب HTTP وهمي) هي النقطة المثالية. فهي تختبر Controllers وMiddleware والتحقق من الصحة (Validation) والصلاحيات (Authorization) واستعلامات قاعدة البيانات ومنطق العمل كله في آنٍ واحد. وهي سريعة بما يكفي لتشغيلها مع كل Commit، وشاملة بما يكفي لاكتشاف الأخطاء الحقيقية.

Feature Tests: استراتيجيتك الأساسية للاختبار

class CreateOrderTest extends TestCase
{
    use RefreshDatabase;

    public function test_authenticated_user_can_create_an_order(): void
    {
        $user = User::factory()->create();
        $product = Product::factory()->create(['price' => 2999, 'stock' => 10]);

        $response = $this->actingAs($user)
            ->postJson('/api/orders', [
                'items' => [
                    ['product_id' => $product->id, 'quantity' => 2],
                ],
                'shipping_address' => '123 Main St',
            ]);

        $response
            ->assertStatus(201)
            ->assertJsonStructure([
                'data' => ['id', 'total', 'status', 'items'],
            ])
            ->assertJsonPath('data.total', 5998)
            ->assertJsonPath('data.status', 'pending');

        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'total' => 5998,
        ]);

        $this->assertDatabaseHas('order_items', [
            'product_id' => $product->id,
            'quantity' => 2,
        ]);

        // Verify stock was decremented
        $this->assertEquals(8, $product->fresh()->stock);
    }

    public function test_guest_cannot_create_an_order(): void
    {
        $response = $this->postJson('/api/orders', [
            'items' => [['product_id' => 1, 'quantity' => 1]],
        ]);

        $response->assertStatus(401);
    }

    public function test_validation_rejects_empty_items(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)
            ->postJson('/api/orders', [
                'items' => [],
            ]);

        $response
            ->assertStatus(422)
            ->assertJsonValidationErrors(['items']);
    }

    public function test_cannot_order_more_than_available_stock(): void
    {
        $user = User::factory()->create();
        $product = Product::factory()->create(['stock' => 2]);

        $response = $this->actingAs($user)
            ->postJson('/api/orders', [
                'items' => [
                    ['product_id' => $product->id, 'quantity' => 5],
                ],
            ]);

        $response->assertStatus(422);
        $this->assertDatabaseCount('orders', 0);
    }
}

اختبار الخدمات الخارجية

لا تستدعِ واجهات API خارجية حقيقية في الاختبارات أبدًا. استخدم Fakes أو Mocks أو أدوات المحاكاة المدمجة في Laravel:

class PaymentProcessingTest extends TestCase
{
    use RefreshDatabase;

    public function test_successful_payment_creates_transaction(): void
    {
        // Arrange: fake the payment gateway
        $this->mock(PaymentGateway::class, function ($mock) {
            $mock->shouldReceive('charge')
                ->once()
                ->with(
                    Mockery::on(fn (Money $m) => $m->inCents() === 5000),
                    Mockery::type(PaymentMethod::class)
                )
                ->andReturn(PaymentResult::success('txn_abc123', new Money(5000)));
        });

        $user = User::factory()->create();
        $order = Order::factory()->for($user)->create(['total' => 5000]);

        // Act
        $response = $this->actingAs($user)
            ->postJson("/api/orders/{$order->id}/pay", [
                'payment_method' => 'pm_test_visa',
            ]);

        // Assert
        $response->assertStatus(200);
        $this->assertEquals('paid', $order->fresh()->status);
    }
}

// Testing email sending
class WelcomeEmailTest extends TestCase
{
    public function test_welcome_email_is_sent_on_registration(): void
    {
        Mail::fake();

        $this->postJson('/api/register', [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ]);

        Mail::assertSent(WelcomeEmail::class, function ($mail) {
            return $mail->hasTo('john@example.com');
        });
    }
}

// Testing queued jobs
class OrderProcessingTest extends TestCase
{
    public function test_order_creation_dispatches_sync_job(): void
    {
        Queue::fake();

        // ... create order

        Queue::assertPushed(SyncOrderToErp::class, function ($job) use ($order) {
            return $job->orderId === $order->id;
        });
    }
}

استراتيجيات اختبار قاعدة البيانات

// RefreshDatabase: migrates once, wraps each test in a transaction
// Best for most test suites
class MyTest extends TestCase
{
    use RefreshDatabase;
}

// Factories: generate realistic test data
class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'password' => Hash::make('password'),
        ];
    }

    // State methods for specific scenarios
    public function admin(): self
    {
        return $this->state(['role' => 'admin']);
    }

    public function suspended(): self
    {
        return $this->state(['suspended_at' => now()]);
    }
}

// Usage in tests
$admin = User::factory()->admin()->create();
$suspended = User::factory()->suspended()->create();
$users = User::factory()->count(50)->create();

اختبار الصلاحيات

class AdminAccessTest extends TestCase
{
    use RefreshDatabase;

    public function test_admin_can_delete_users(): void
    {
        $admin = User::factory()->admin()->create();
        $user = User::factory()->create();

        $response = $this->actingAs($admin)
            ->deleteJson("/api/users/{$user->id}");

        $response->assertStatus(204);
        $this->assertSoftDeleted('users', ['id' => $user->id]);
    }

    public function test_regular_user_cannot_delete_users(): void
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();

        $response = $this->actingAs($user)
            ->deleteJson("/api/users/{$otherUser->id}");

        $response->assertStatus(403);
        $this->assertDatabaseHas('users', ['id' => $otherUser->id]);
    }
}

ما الذي يجب اختباره (وما الذي يمكن تخطيه)

  • اختبر دائمًا: منطق العمل الحرج، قواعد الصلاحيات، قواعد التحقق من الصحة، الحالات الحدية في العمليات الحسابية، عقود API
  • اختبر غالبًا: استعلامات قاعدة البيانات المعقدة، إرسال Jobs والأحداث (Events)، إرسال الإشعارات
  • تخطَّ: الأجزاء الداخلية للإطار (Framework)، الـ Getters والـ Setters البسيطة، تعريفات علاقات Eloquent، الكود البديهي الذي يضمنه الإطار

جعل الاختبارات قابلة للصيانة

اكتب اختبارات تصف السلوك وليس التنفيذ. إذا أعدت هيكلة الأجزاء الداخلية وفشلت اختباراتك رغم أن السلوك لم يتغير، فإن اختباراتك مرتبطة بالتنفيذ أكثر مما ينبغي.

  • سمِّ الاختبارات بصيغة test_user_can_do_something -- فهي تُقرأ كتوثيق
  • استخدم نمط AAA: ترتيب (Arrange)، تنفيذ (Act)، تأكيد (Assert) -- مفصولة بوضوح
  • لا تختبر الدوال الخاصة (Private Methods) مباشرةً -- اختبرها من خلال الواجهة العامة (Public API)
  • أبقِ كل اختبار مستقلًا -- لا يجب أن يعتمد أي اختبار على حالة اختبار آخر
  • شغّل اختباراتك على CI مع كل Push. الاختبارات التي لا تُشغَّل بانتظام تصبح قديمة وغير موثوقة.
العودة لجميع المقالات