الاختبارات الآلية في Laravel
بنيت API كاملاً في الدرس السابق، وجرّبته بيدك طلباً طلباً. لكن بعد شهر ستعدّل شيئاً صغيراً في التحقق أو في قاعدة البيانات: هل ستعيد تجربة كل المسارات يدوياً؟ في هذا الدرس، وهو الأخير في الكورس، ستكتب اختبارات آلية (Automated Tests): كود يجرّب تطبيقك بدلاً منك في ثوانٍ، ويخبرك فوراً إن كسرت شيئاً.
لماذا نكتب الاختبارات؟
Section titled “لماذا نكتب الاختبارات؟”الطيار لا يعتمد على ذاكرته قبل الإقلاع: يمر على قائمة فحص مكتوبة، بنداً بنداً، في كل رحلة. الاختبارات هي قائمة فحص تطبيقك، والفرق أن الحاسوب هو من يمر عليها.
Laravel يأتي مجهزاً لذلك. جرّب الآن في مشروعك:
php artisan testسترى اختبارين جاهزين ينجحان. والاختبارات تعيش في مجلدين:
| المجلد | النوع | ماذا يختبر |
|---|---|---|
tests/Feature |
اختبار ميزة | طلباً كاملاً: المسار والمتحكم وقاعدة البيانات معاً |
tests/Unit |
اختبار وحدة | دالة أو كلاساً واحداً بمعزل عن التطبيق |
سنركز على اختبارات الميزات، فهي الأقرب لما يفعله المستخدم.
أول اختبار
Section titled “أول اختبار”php artisan make:test PostApiTestالأمر ينشئ tests/Feature/PostApiTest.php. اكتب فيه:
<?php
namespace Tests\Feature;
use App\Models\Post;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;
class PostApiTest extends TestCase{ use RefreshDatabase;
public function test_posts_list_returns_all_posts(): void { Post::factory()->count(3)->create();
$response = $this->getJson('/api/posts');
$response->assertOk(); $response->assertJsonCount(3, 'data'); }}كل اختبار جيد ثلاث خطوات: جهّز، ثم نفّذ، ثم تحقق.
- جهّز: المصنع الذي كتبته في درس الـ Migrations ينشئ ثلاثة مقالات وهمية.
- نفّذ:
getJsonترسل طلب GET ومعه الترويسةAccept: application/json، دون تشغيل خادم ولا متصفح. - تحقق:
assertOkتتأكد أن الرمز 200، وassertJsonCountتتأكد أن في المفتاحdataثلاثة عناصر.
والسطر use RefreshDatabase يبني الجداول قبل الاختبارات، ويمسح بيانات كل اختبار بعد انتهائه. فكل اختبار يبدأ بقاعدة بيانات فارغة، ولا يتأثر بما قبله.
اختبار الإضافة والتحقق
Section titled “اختبار الإضافة والتحقق”أضف هاتين الدالتين إلى الكلاس نفسه:
public function test_post_can_be_created(): void{ $response = $this->postJson('/api/posts', [ 'title' => 'مقال من الاختبار', 'body' => 'نص طويل كفاية ليتجاوز شرط العشرين حرفاً.', ]);
$response->assertCreated(); $response->assertJsonPath('data.title', 'مقال من الاختبار'); $this->assertDatabaseHas('posts', ['title' => 'مقال من الاختبار']);}
public function test_short_title_is_rejected(): void{ $response = $this->postJson('/api/posts', [ 'title' => 'قصير', 'body' => 'نص طويل كفاية ليتجاوز شرط العشرين حرفاً.', ]);
$response->assertUnprocessable(); $response->assertJsonValidationErrors(['title']); $this->assertDatabaseCount('posts', 0);}الاختبار الأول لا يكتفي بالرد: assertDatabaseHas تفتح الجدول وتتأكد أن المقال حُفظ فعلاً. والثاني يختبر طريق الفشل: عنوان قصير يجب أن يُرفض بالرمز 422، وألا يُحفظ شيء. اختبار الحالات الخاطئة لا يقل أهمية عن اختبار الصحيحة.
وأخيراً المقال غير الموجود والحذف:
public function test_missing_post_returns_404(): void{ $this->getJson('/api/posts/999')->assertNotFound();}
public function test_post_can_be_deleted(): void{ $post = Post::factory()->create();
$this->deleteJson("/api/posts/{$post->id}")->assertNoContent();
$this->assertDatabaseMissing('posts', ['id' => $post->id]);}شغّل اختبارات هذا الملف وحده:
php artisan test --filter=PostApiTest PASS Tests\Feature\PostApiTest ✓ posts list returns all posts 0.35s ✓ post can be created 0.02s ✓ short title is rejected 0.02s ✓ missing post returns 404 0.01s ✓ post can be deleted 0.01s
Tests: 5 passed (13 assertions) Duration: 0.72sخمسة مسارات جُرّبت مع قاعدة البيانات في أقل من ثانية. Laravel حوّل اسم كل دالة إلى جملة مقروءة، فاختر أسماء تصف السلوك المتوقع.
عندما يفشل اختبار
Section titled “عندما يفشل اختبار”بعد أسابيع يعدّل زميل قاعدة التحقق من min:5 إلى min:3 دون أن ينتبه. يشغّل الاختبارات قبل الرفع فيرى:
FAIL Tests\Feature\PostApiTest ✓ posts list returns all posts 0.37s ✓ post can be created 0.02s ⨯ short title is rejected 0.02s ✓ missing post returns 404 0.02s ✓ post can be deleted 0.01s ────────────────────────────────────────────────────────── FAILED Tests\Feature\PostApiTest > short title is rejected Expected response status code [422] but received 201.
at tests\Feature\PostApiTest.php:42
Tests: 1 failed, 4 passed (10 assertions)التقرير يقول كل شيء: أي اختبار فشل، وماذا توقعنا (422)، وماذا حصل (201)، وفي أي سطر. الخطأ اكتُشف في ثانية، قبل أن يصل إلى المستخدمين. هذه هي شبكة الأمان التي تجعلك تعدّل الكود بثقة.
اختبار الصفحات المحمية
Section titled “اختبار الصفحات المحمية”في درس الـ Middleware حميت /dashboard بالوسيط auth، وصفحة /admin بوسيطك الخاص. كيف تختبر صفحة تحتاج تسجيل دخول؟ الدالة actingAs تجعل الطلب يصدر كأنه من مستخدم معيّن:
<?php
namespace Tests\Feature;
use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;
class DashboardTest extends TestCase{ use RefreshDatabase;
public function test_guest_is_redirected_to_login(): void { $this->get('/dashboard')->assertRedirect('/login'); }
public function test_logged_in_user_sees_dashboard(): void { $user = User::factory()->create();
$this->actingAs($user)->get('/dashboard')->assertOk(); }
public function test_normal_user_cannot_open_admin_page(): void { $user = User::factory()->create();
$this->actingAs($user)->get('/admin')->assertForbidden(); }
public function test_admin_can_open_admin_page(): void { $admin = User::factory()->create(['is_admin' => true]);
$this->actingAs($admin)->get('/admin')->assertOk()->assertSee('لوحة المدير'); }}أربعة اختبارات تغطي كل الحالات: زائر، ومستخدم عادي، ومدير. لاحظ أن المصنع قبل is_admin مع أن العمود خارج القائمة المسموح ملؤها: المصانع تتجاوز هذه الحماية لأنها كود تكتبه أنت، لا بيانات قادمة من زائر.
أشهر دوال التحقق
Section titled “أشهر دوال التحقق”| الدالة | تتأكد من |
|---|---|
assertOk و assertCreated و assertNoContent |
الرموز 200 و 201 و 204 |
assertNotFound و assertForbidden و assertUnprocessable |
الرموز 404 و 403 و 422 |
assertRedirect('/login') |
تحويل إلى عنوان معيّن |
assertSee('نص') |
وجود نص في الصفحة |
assertJsonPath('data.title', '...') |
قيمة مفتاح داخل JSON |
assertJsonValidationErrors(['title']) |
وجود خطأ تحقق لحقل |
assertDatabaseHas و assertDatabaseMissing |
وجود صف في جدول أو غيابه |
جرّب بنفسك
Section titled “جرّب بنفسك”- اكتب اختباراً لتعديل مقال بـ
putJson، وتأكد بـassertDatabaseHasأن العنوان الجديد حُفظ. - غيّر
min:20في المتحكم إلىmin:200وشغّل الاختبارات. أي اختبار فشل؟ ولماذا؟ ثم أعد القيمة. - اختبر صفحة الدخول من درس الـ Middleware: أرسل بيانات صحيحة بـ
post('/login', [...])وتأكد بـassertAuthenticated()، ثم بيانات خاطئة وتأكد بـassertGuest(). - تحدٍّ: اختبر الترقيم: أنشئ 12 مقالاً وتأكد أن الصفحة الأولى فيها 10 والثانية (
?page=2) فيها 2.
أخطاء شائعة
Section titled “أخطاء شائعة”- اسم دالة لا يبدأ بـ
test: الدالة تُتجاهل بصمت ولا تُشغَّل أبداً. ابدأ الاسم بـtest_. - نسيان
RefreshDatabase: يظهر خطأ أن الجدول غير موجود، أو تتسرب بيانات اختبار إلى آخر. - اختبار يعتمد على اختبار قبله: الترتيب غير مضمون. كل اختبار يجهّز بياناته بنفسه.
- اختبار طريق النجاح فقط: أغلب الثغرات في الحالات الخاطئة: بيانات ناقصة، وزائر غير مسجّل، وعنصر غير موجود.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما فائدة use RefreshDatabase في كلاس الاختبار؟
تبني الجداول وتمسح ما يكتبه كل اختبار، فلا يتأثر اختبار ببيانات اختبار آخر.
سؤال
كيف تختبر صفحة محمية بالوسيط auth كأنك مستخدم مسجّل؟
actingAs تجعل الطلب يصدر باسم المستخدم المعطى، دون المرور بصفحة الدخول.
الخلاصة
Section titled “الخلاصة”- الاختبارات الآلية كود يجرّب تطبيقك بدلاً منك، وتشغّلها بـ
php artisan test. - كل اختبار ثلاث خطوات: جهّز البيانات، ونفّذ الطلب، وتحقق من النتيجة.
RefreshDatabaseوالمصانع يعطيان كل اختبار قاعدة بيانات نظيفة وبيانات جاهزة.- اختبر الرد وقاعدة البيانات معاً، واختبر حالات الفشل كما تختبر النجاح.
actingAsتختبر الصفحات المحمية باسم أي مستخدم.
مبروك! أنهيت كورس Laravel كاملاً. عندك الآن كل ما تحتاجه لبناء تطبيق حقيقي: مسارات ومتحكمات وقوالب وقاعدة بيانات مترابطة وحماية و API واختبارات تحرسه. ابنِ مدونتك الكاملة، وعُد إلى التوثيق الرسمي كلما احتجت ميزة جديدة.