تخطَّ إلى المحتوى

الاختبارات الآلية في Laravel

الدرس 11 من 11 · Laravel

بنيت API كاملاً في الدرس السابق، وجرّبته بيدك طلباً طلباً. لكن بعد شهر ستعدّل شيئاً صغيراً في التحقق أو في قاعدة البيانات: هل ستعيد تجربة كل المسارات يدوياً؟ في هذا الدرس، وهو الأخير في الكورس، ستكتب اختبارات آلية (Automated Tests): كود يجرّب تطبيقك بدلاً منك في ثوانٍ، ويخبرك فوراً إن كسرت شيئاً.

لماذا نكتب الاختبارات؟

Section titled “لماذا نكتب الاختبارات؟”

الطيار لا يعتمد على ذاكرته قبل الإقلاع: يمر على قائمة فحص مكتوبة، بنداً بنداً، في كل رحلة. الاختبارات هي قائمة فحص تطبيقك، والفرق أن الحاسوب هو من يمر عليها.

Laravel يأتي مجهزاً لذلك. جرّب الآن في مشروعك:

نافذة طرفيّة
php artisan test

سترى اختبارين جاهزين ينجحان. والاختبارات تعيش في مجلدين:

المجلد النوع ماذا يختبر
tests/Feature اختبار ميزة طلباً كاملاً: المسار والمتحكم وقاعدة البيانات معاً
tests/Unit اختبار وحدة دالة أو كلاساً واحداً بمعزل عن التطبيق

سنركز على اختبارات الميزات، فهي الأقرب لما يفعله المستخدم.

نافذة طرفيّة
php artisan make:test PostApiTest

الأمر ينشئ tests/Feature/PostApiTest.php. اكتب فيه:

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 حوّل اسم كل دالة إلى جملة مقروءة، فاختر أسماء تصف السلوك المتوقع.

بعد أسابيع يعدّل زميل قاعدة التحقق من 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 تجعل الطلب يصدر كأنه من مستخدم معيّن:

tests/Feature/DashboardTest.php
<?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 مع أن العمود خارج القائمة المسموح ملؤها: المصانع تتجاوز هذه الحماية لأنها كود تكتبه أنت، لا بيانات قادمة من زائر.

الدالة تتأكد من
assertOk و assertCreated و assertNoContent الرموز 200 و 201 و 204
assertNotFound و assertForbidden و assertUnprocessable الرموز 404 و 403 و 422
assertRedirect('/login') تحويل إلى عنوان معيّن
assertSee('نص') وجود نص في الصفحة
assertJsonPath('data.title', '...') قيمة مفتاح داخل JSON
assertJsonValidationErrors(['title']) وجود خطأ تحقق لحقل
assertDatabaseHas و assertDatabaseMissing وجود صف في جدول أو غيابه
  1. اكتب اختباراً لتعديل مقال بـ putJson، وتأكد بـ assertDatabaseHas أن العنوان الجديد حُفظ.
  2. غيّر min:20 في المتحكم إلى min:200 وشغّل الاختبارات. أي اختبار فشل؟ ولماذا؟ ثم أعد القيمة.
  3. اختبر صفحة الدخول من درس الـ Middleware: أرسل بيانات صحيحة بـ post('/login', [...]) وتأكد بـ assertAuthenticated()، ثم بيانات خاطئة وتأكد بـ assertGuest().
  4. تحدٍّ: اختبر الترقيم: أنشئ 12 مقالاً وتأكد أن الصفحة الأولى فيها 10 والثانية (?page=2) فيها 2.
  • اسم دالة لا يبدأ بـ test: الدالة تُتجاهل بصمت ولا تُشغَّل أبداً. ابدأ الاسم بـ test_.
  • نسيان RefreshDatabase: يظهر خطأ أن الجدول غير موجود، أو تتسرب بيانات اختبار إلى آخر.
  • اختبار يعتمد على اختبار قبله: الترتيب غير مضمون. كل اختبار يجهّز بياناته بنفسه.
  • اختبار طريق النجاح فقط: أغلب الثغرات في الحالات الخاطئة: بيانات ناقصة، وزائر غير مسجّل، وعنصر غير موجود.

سؤال

ما فائدة use RefreshDatabase في كلاس الاختبار؟

سؤال

كيف تختبر صفحة محمية بالوسيط auth كأنك مستخدم مسجّل؟

  • الاختبارات الآلية كود يجرّب تطبيقك بدلاً منك، وتشغّلها بـ php artisan test.
  • كل اختبار ثلاث خطوات: جهّز البيانات، ونفّذ الطلب، وتحقق من النتيجة.
  • RefreshDatabase والمصانع يعطيان كل اختبار قاعدة بيانات نظيفة وبيانات جاهزة.
  • اختبر الرد وقاعدة البيانات معاً، واختبر حالات الفشل كما تختبر النجاح.
  • actingAs تختبر الصفحات المحمية باسم أي مستخدم.

مبروك! أنهيت كورس Laravel كاملاً. عندك الآن كل ما تحتاجه لبناء تطبيق حقيقي: مسارات ومتحكمات وقوالب وقاعدة بيانات مترابطة وحماية و API واختبارات تحرسه. ابنِ مدونتك الكاملة، وعُد إلى التوثيق الرسمي كلما احتجت ميزة جديدة.