الاختبارات و TDD

اختبار تطبيقات Laravel: الاختبارات والمصانع واختبار HTTP

22 دقيقة الدرس 10 من 35

نظرة عامة على اختبار Laravel

يوفر Laravel دعماً ممتازاً مدمجاً للاختبار يجعل اختبار تطبيقات PHP ممتعاً ومنتجاً. بُني فوق PHPUnit، يضيف Laravel مساعدين اختبار مريحين وميزات إدارة قاعدة البيانات وأدوات اختبار HTTP وواجهة برمجية نظيفة لاختبار كل شيء من الوحدات البسيطة إلى سير عمل الميزات الكاملة. أدوات اختبار Laravel مصممة لجعل الاختبار يبدو طبيعياً ومتكاملاً في سير عمل التطوير الخاص بك.

يدعم Laravel نوعين من الاختبارات خارج الصندوق: اختبارات الوحدة لاختبار أجزاء صغيرة ومعزولة من الوظائف، واختبارات الميزات لاختبار أجزاء أكبر من تطبيقك بما في ذلك طلبات HTTP وعمليات قاعدة البيانات والتفاعلات بين مكونات متعددة. يوفر الإطار كل ما تحتاجه: مصانع لتوليد بيانات الاختبار، وRefreshDatabase لحالات اختبار نظيفة، ومساعدي اختبار HTTP لاختبار API، ومساعدي المصادقة لاختبار المسارات المحمية.

ميزات اختبار Laravel:
  • تكامل PHPUnit: مبني على PHPUnit مع مساعدين إضافيين
  • إدارة قاعدة البيانات: RefreshDatabase وسمات DatabaseTransactions
  • اختبار HTTP: اختبار نقاط نهاية API والمسارات بسهولة
  • المصانع: توليد بيانات وهمية للاختبار
  • المصادقة: انتحال شخصية المستخدم بسهولة في الاختبارات
  • أوامر Artisan: تشغيل الاختبارات باستخدام `php artisan test`

إعداد اختبارات Laravel

بنية الاختبار

ينظم Laravel الاختبارات في مجلدين:

tests/ ├── Feature/ # اختبارات الميزات (اختبار الميزات الكاملة) │ └── UserTest.php └── Unit/ # اختبارات الوحدة (اختبار الفئات/الطرق الفردية) └── UserServiceTest.php

إنشاء الاختبارات

# إنشاء اختبار وحدة php artisan make:test UserServiceTest --unit # إنشاء اختبار ميزة php artisan make:test UserApiTest # تشغيل الاختبارات php artisan test # التشغيل مع التغطية php artisan test --coverage # تشغيل ملف اختبار محدد php artisan test tests/Feature/UserTest.php # تشغيل طريقة اختبار محددة php artisan test --filter test_user_can_register

بنية الاختبار الأساسية

<?php namespace Tests\Feature; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class UserTest extends TestCase { use RefreshDatabase; public function test_user_can_register() { $response = $this->post('/api/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', 'password_confirmation' => 'password123' ]); $response->assertStatus(201); $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); } }

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

سمة RefreshDatabase

تقوم سمة RefreshDatabase بتشغيل الترحيلات قبل كل اختبار وإرجاعها بعد ذلك، مما يضمن حالة قاعدة بيانات نظيفة:

<?php namespace Tests\Feature; use Tests\TestCase; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; class UserTest extends TestCase { use RefreshDatabase; public function test_user_can_be_created() { $user = User::create([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => bcrypt('password123') ]); $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); } public function test_user_can_be_updated() { $user = User::factory()->create(); $user->update(['name' => 'Jane Doe']); $this->assertDatabaseHas('users', [ 'id' => $user->id, 'name' => 'Jane Doe' ]); } public function test_user_can_be_deleted() { $user = User::factory()->create(); $user->delete(); $this->assertDatabaseMissing('users', [ 'id' => $user->id ]); } }

سمة DatabaseTransactions

للاختبارات الأسرع، استخدم DatabaseTransactions لتغليف كل اختبار في معاملة وإرجاعها:

<?php use Illuminate\Foundation\Testing\DatabaseTransactions; class UserTest extends TestCase { use DatabaseTransactions; // الاختبارات تعمل داخل المعاملات ويتم إرجاعها }
RefreshDatabase مقابل DatabaseTransactions:
  • RefreshDatabase: أبطأ، لكن أكثر شمولاً. يشغل الترحيلات، جيد لاختبار الترحيلات نفسها
  • DatabaseTransactions: أسرع، يغلف الاختبارات في معاملات. لا يمكن اختبار المعاملات في الكود الخاص بك

تأكيدات قاعدة البيانات

// تأكيد وجود السجل $this->assertDatabaseHas('users', [ 'email' => 'john@example.com', 'name' => 'John Doe' ]); // تأكيد عدم وجود السجل $this->assertDatabaseMissing('users', [ 'email' => 'deleted@example.com' ]); // تأكيد العدد $this->assertDatabaseCount('users', 10); // تأكيد الحذف الناعم $this->assertSoftDeleted('users', [ 'id' => $user->id ]); // تأكيد عدم الحذف الناعم $this->assertNotSoftDeleted('users', [ 'id' => $user->id ]);

مصانع النماذج

إنشاء المصانع

# توليد مصنع php artisan make:factory UserFactory --model=User
<?php namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; class UserFactory extends Factory { public function definition() { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => bcrypt('password'), 'remember_token' => Str::random(10), ]; } // حالات المصنع public function unverified() { return $this->state(fn (array $attributes) => [ 'email_verified_at' => null, ]); } public function admin() { return $this->state(fn (array $attributes) => [ 'role' => 'admin', ]); } }

استخدام المصانع

// إنشاء مستخدم واحد $user = User::factory()->create(); // إنشاء مستخدمين متعددين $users = User::factory()->count(10)->create(); // الإنشاء بسمات محددة $user = User::factory()->create([ 'email' => 'john@example.com', 'name' => 'John Doe' ]); // استخدام حالات المصنع $unverifiedUser = User::factory()->unverified()->create(); $adminUser = User::factory()->admin()->create(); // ربط الحالات $user = User::factory() ->unverified() ->admin() ->create(); // الإنشاء دون الحفظ في قاعدة البيانات $user = User::factory()->make();

العلاقات في المصانع

<?php // مصنع المنشورات class PostFactory extends Factory { public function definition() { return [ 'user_id' => User::factory(), 'title' => fake()->sentence(), 'content' => fake()->paragraphs(3, true), 'published_at' => now(), ]; } } // الاستخدام $post = Post::factory()->create(); // ينشئ المستخدم تلقائياً $post = Post::factory() ->for(User::factory()->admin()) ->create(); // ينشئ منشوراً لمستخدم مسؤول // علاقات has many $user = User::factory() ->has(Post::factory()->count(5)) ->create(); // ينشئ مستخدماً مع 5 منشورات // صيغة بديلة $user = User::factory() ->hasPosts(5) ->create(); // علاقات many to many $user = User::factory() ->hasAttached(Role::factory()->count(3)) ->create();

اختبار HTTP

إجراء الطلبات

// طلب GET $response = $this->get('/api/users'); // طلب POST $response = $this->post('/api/users', [ 'name' => 'John Doe', 'email' => 'john@example.com' ]); // طلب PUT $response = $this->put('/api/users/1', [ 'name' => 'Jane Doe' ]); // طلب PATCH $response = $this->patch('/api/users/1', [ 'name' => 'Jane Doe' ]); // طلب DELETE $response = $this->delete('/api/users/1'); // مع ترويسات $response = $this->withHeaders([ 'X-Custom-Header' => 'value' ])->get('/api/users'); // طلب JSON $response = $this->postJson('/api/users', [ 'name' => 'John Doe' ]);

تأكيدات الاستجابة

// تأكيدات الحالة $response->assertStatus(200); $response->assertOk(); // 200 $response->assertCreated(); // 201 $response->assertNoContent(); // 204 $response->assertNotFound(); // 404 $response->assertForbidden(); // 403 $response->assertUnauthorized(); // 401 // تأكيدات JSON $response->assertJson([ 'name' => 'John Doe', 'email' => 'john@example.com' ]); $response->assertJsonStructure([ 'data' => [ '*' => ['id', 'name', 'email'] ] ]); $response->assertJsonPath('user.name', 'John Doe'); $response->assertJsonCount(10, 'data'); // تأكيدات الترويسة $response->assertHeader('Content-Type', 'application/json'); // تأكيدات إعادة التوجيه $response->assertRedirect('/dashboard'); // تأكيدات العرض $response->assertViewIs('users.index'); $response->assertViewHas('users');

اختبار JSON APIs

public function test_can_list_users() { User::factory()->count(3)->create(); $response = $this->getJson('/api/users'); $response ->assertStatus(200) ->assertJsonCount(3, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'name', 'email'] ] ]); } public function test_can_create_user() { $userData = [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123' ]; $response = $this->postJson('/api/users', $userData); $response ->assertStatus(201) ->assertJson([ 'name' => 'John Doe', 'email' => 'john@example.com' ]); $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); } public function test_validation_fails_for_invalid_email() { $response = $this->postJson('/api/users', [ 'name' => 'John Doe', 'email' => 'invalid-email' ]); $response ->assertStatus(422) ->assertJsonValidationErrors(['email']); }

اختبار المصادقة

التصرف كمستخدم

public function test_authenticated_user_can_access_dashboard() { $user = User::factory()->create(); $response = $this->actingAs($user) ->get('/dashboard'); $response->assertStatus(200); } public function test_guest_cannot_access_dashboard() { $response = $this->get('/dashboard'); $response->assertRedirect('/login'); } // مع حارس محدد $response = $this->actingAs($user, 'api') ->get('/api/user'); // مع Sanctum use Laravel\Sanctum\Sanctum; Sanctum::actingAs($user); $response = $this->getJson('/api/user');

اختبار التفويض

public function test_admin_can_delete_users() { $admin = User::factory()->admin()->create(); $user = User::factory()->create(); $response = $this->actingAs($admin) ->delete("/api/users/{$user->id}"); $response->assertStatus(204); $this->assertDatabaseMissing('users', [ 'id' => $user->id ]); } public function test_regular_user_cannot_delete_users() { $user = User::factory()->create(); $otherUser = User::factory()->create(); $response = $this->actingAs($user) ->delete("/api/users/{$otherUser->id}"); $response->assertStatus(403); }

اختبار ميزات Laravel

اختبار الأحداث

use Illuminate\Support\Facades\Event; public function test_user_registration_dispatches_event() { Event::fake([UserRegistered::class]); $this->post('/api/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123' ]); Event::assertDispatched(UserRegistered::class, function ($event) { return $event->user->email === 'john@example.com'; }); }

اختبار المهام

use Illuminate\Support\Facades\Queue; public function test_email_job_is_dispatched() { Queue::fake(); $user = User::factory()->create(); $this->post('/api/send-email', [ 'user_id' => $user->id ]); Queue::assertPushed(SendEmailJob::class, function ($job) use ($user) { return $job->user->id === $user->id; }); }

اختبار البريد

use Illuminate\Support\Facades\Mail; public function test_welcome_email_is_sent() { Mail::fake(); $user = User::factory()->create(); $this->post('/api/register', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123' ]); Mail::assertSent(WelcomeEmail::class, function ($mail) { return $mail->hasTo('john@example.com'); }); }

اختبار الإشعارات

use Illuminate\Support\Facades\Notification; public function test_notification_is_sent() { Notification::fake(); $user = User::factory()->create(); $user->notify(new OrderShipped($order)); Notification::assertSentTo($user, OrderShipped::class); }

اختبار التخزين

use Illuminate\Support\Facades\Storage; use Illuminate\Http\UploadedFile; public function test_file_upload() { Storage::fake('public'); $file = UploadedFile::fake()->image('avatar.jpg'); $response = $this->post('/api/upload', [ 'file' => $file ]); Storage::disk('public')->assertExists('avatars/' . $file->hashName()); }
تمرين تطبيقي:
  1. أنشئ نموذج Task مع العنوان والوصف وحالة الإكمال وعلاقة المستخدم
  2. أنشئ TaskFactory مع بيانات وهمية مناسبة
  3. اكتب اختبارات وحدة لطرق نموذج Task (markAsCompleted، markAsIncomplete)
  4. اكتب اختبارات ميزات لنقاط نهاية API لـ CRUD للمهام (إنشاء، قراءة، تحديث، حذف)
  5. اختبر أن المستخدمين يمكنهم الوصول إلى مهامهم فقط
  6. اختبر قواعد التحقق لإنشاء المهمة (العنوان مطلوب، الوصف اختياري)
  7. اختبر أن حدث TaskCompleted يتم إطلاقه عندما يتم وضع علامة على المهمة كمكتملة
  8. تأكد من أن جميع الاختبارات تستخدم المصانع وسمة RefreshDatabase

أفضل الممارسات

1. استخدم المصانع لبيانات الاختبار

// سيء - بيانات اختبار ثابتة $user = User::create([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => bcrypt('password') ]); // جيد - استخدم المصانع $user = User::factory()->create();

2. اختبر شيئاً واحداً لكل اختبار

// سيء - يختبر الكثير public function test_user_crud() { $user = User::factory()->create(); // اختبار الإنشاء والتحديث والحذف في واحد } // جيد - اختبارات منفصلة public function test_user_can_be_created() { } public function test_user_can_be_updated() { } public function test_user_can_be_deleted() { }

3. استخدم أسماء اختبار وصفية

// أسماء اختبار جيدة public function test_user_can_register_with_valid_data() { } public function test_registration_fails_with_duplicate_email() { } public function test_admin_can_delete_users() { } public function test_guest_cannot_access_dashboard() { }
المخاطر الشائعة:
  • نسيان استخدام سمة RefreshDatabase - الاختبارات تؤثر على بعضها البعض
  • عدم استخدام المصانع - اختبارات هشة وصعبة الصيانة
  • اختبار كود الإطار بدلاً من الكود الخاص بك
  • عدم التنظيف بعد الاختبارات (الملفات، سجلات قاعدة البيانات)
  • اختبار الكثير في اختبار واحد - يجعل التصحيح أصعب

الخلاصة

يوفر Laravel مجموعة أدوات اختبار شاملة مبنية على PHPUnit تجعل اختبار تطبيقات PHP منتجاً وممتعاً. مع RefreshDatabase لحالات اختبار نظيفة، والمصانع لتوليد بيانات الاختبار، ومساعدي اختبار HTTP لاختبار API، وطرق تأكيد مريحة، يزيل Laravel الاحتكاك من الاختبار. أدوات الاختبار في الإطار تتكامل بسلاسة مع سير عمل التطوير الخاص بك، وتشجع التطوير الموجه بالاختبار وتساعدك على بناء تطبيقات أكثر موثوقية.

أتقن أساسيات اختبار Laravel هذه - المصانع وإدارة قاعدة البيانات واختبار HTTP والمصادقة - وستكون مجهزاً جيداً لكتابة مجموعات اختبار شاملة تمنحك الثقة في الكود الخاص بك. تذكر: الاختبارات الجيدة سريعة ومعزولة ومركزة. استخدم المصانع بحرية، واحتفظ بالاختبارات مستقلة مع RefreshDatabase، واختبر سيناريوهات النجاح والفشل لبناء تطبيقات Laravel قوية ومختبرة جيداً.