موضع نص الإشعار اتصل بنا تفضل الآن!

كيف قادني Laravel إلى حب الـ Testing رغم كرهي له سابقا

George Bahgat

كيف قادني Laravel إلى حب الـ Testing رغم كرهي له سابقًا

في بداياتي مع البرمجة، كان مفهوم "الاختبار" (Testing) من أكثر الأمور التي أتجنبها بحرص شديد. كنت أرى فيه عبئًا زائدًا، وقتًا ضائعًا، وتعقيدًا غير ضروري. كل ما كنت أريده هو كتابة الكود، تشغيله، التأكد من أنه يعمل على جهازي، ثم تسليمه. كنت أقول لنفسي: "طالما الكود شغال، ليش أضيع وقتي في كتابة اختبارات له؟". لكن مع مرور الوقت، ومع تزايد تعقيد المشاريع التي أعمل عليها، بدأت أدرك أن هذا التفكير كان قصير النظر، بل وخطير أحيانًا. والغريب أن التحوّل الحقيقي في نظرتي حدث حين بدأت أستخدم إطار عمل Laravel.

لارافيل لم يُجبرني على كتابة اختبارات، بل جعلني أحبها. لم يفرضها كواجب تقني جاف، بل قدّمها كأداة ذكية تُسهّل حياتي، تحميني من الأخطاء، وتوفر لي وقتًا ثمينًا في المدى الطويل. في هذا المقال، سأروي لك رحلتي من كره الـ Testing إلى تبنيه كجزء لا يتجزأ من عملي اليومي، وكيف ساعدني Laravel على رؤية الصورة الكاملة. سنغطي لماذا يكره المطورون الاختبارات في البداية، وكيف يغيّر Laravel هذه النظرة، مع أمثلة عملية من مشاريع حقيقية، وأكواد توضيحية، ونصائح مبنية على تجربة فعلية.

لماذا كرهت الـ Testing في البداية؟

قبل أن أتعمق في كيف غيّر Laravel نظرتي، من المهم أن أشرح لماذا كنت أكره الـ Testing من الأساس. لم يكن الكُره نابعًا من جهل تام، بل من تجارب سيئة وسوء فهم لدور الاختبارات الحقيقية.

الاختبارات = وقت إضافي بدون فائدة مرئية

في المشاريع الصغيرة أو النماذج الأولية (Prototypes)، كان من السهل رؤية النتيجة مباشرة. أكتب دالة، أجرّبها في المتصفح، وأرى النتيجة. إذا ظهرت كما أردت، فالأمر تمام. كتابة اختبارات لهذا السيناريو بدت لي مضيعة للوقت. كنت أحسب أن كل دقيقة أقضيها في كتابة اختبار هي دقيقة لا أقضيها في "البرمجة الحقيقية". لم أكن أدرك أن هذا الوقت سيُوفّر لي ساعات لاحقًا عندما يكبر المشروع.

الاختبارات معقدة وغامضة

في بداياتي، حاولت مرة كتابة اختبارات باستخدام PHPUnit خارج إطار عمل. كانت التجربة محيرة: ما الفرق بين unit test و feature test؟ كيف أُحاكي قاعدة البيانات؟ كيف أتعامل مع الـ HTTP requests؟ لم تكن هناك بنية واضحة، ولا دليل عملي يشرح لي كيف أبدأ. شعرت أنني أتعلم لغة جديدة بدلًا من التركيز على مشروعي.

الخوف من الفشل المتكرر

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

ملاحظة: كثير من المطورين يتركون الـ Testing لأنهم يعتقدون أنه "للمحترفين فقط". الحقيقة أن الـ Testing أداة لكل مطور، بغض النظر عن مستواه. المهم أن تبدأ بسيطًا.

لارافيل: البوابة التي غيّرت كل شيء

عندما بدأت باستخدام Laravel لأول مرة، لم أكن أنوي كتابة اختبارات. لكن ما لفت انتباهي هو أن Laravel يأتي مُهيّأ مسبقًا للاختبار. الملفات الأساسية موجودة، البيئة جاهزة، والأدوات مدمجة. لم أضطر لتركيب مكتبات خارجية أو قضاء ساعات في التهيئة. كل ما عليّ فعله هو أن أبدأ.

بيئة اختبار جاهزة من الصندوق

في مشروع لارافيل جديد، ستجد مجلد tests موجودًا تلقائيًا. داخله، مجلدان: Unit و Feature. هذا التقسيم البسيط يساعدك على فهم نوعي الاختبارات دون الحاجة لقراءة كتب تقنية. بالإضافة إلى ذلك، ملف phpunit.xml مهيأ مسبقًا ليستخدم قاعدة بيانات اختبار منفصلة (عادة في الذاكرة باستخدام SQLite)، مما يعني أن اختباراتك لن تؤثر على بياناتك الحقيقية.

في أحد المشاريع التي عملت عليها، كنت أبني نظام إدارة محتوى بسيط. في البداية، تجاهلت مجلد tests تمامًا. لكن بعد أسبوعين، بدأت أواجه مشكلة: كل مرة أُجري فيها تعديلًا صغيرًا في منطق النشر، أكتشف لاحقًا أنني كسرت ميزة الحذف. في اليوم الثالث من إصلاح الأخطاء المتكررة، قررت أن أجرب كتابة اختبار بسيط. استخدمت الأمر التالي لإنشاء اختبار ميزة (Feature Test):

php artisan make:test PostManagementTest

النتيجة كانت ملفًا جديدًا في tests/Feature/PostManagementTest.php، يحتوي على هيكل أساسي جاهز. كل ما عليّ فعله هو إضافة طريقة (method) تبدأ بـ test وكتابة سيناريو بسيط.

التكامل السلس مع أدوات Laravel

ما جعل الأمور أسهل هو أن Laravel يوفّر طرقًا مدمجة تُسهّل كتابة الاختبارات بشكل كبير. على سبيل المثال، في Feature Tests، يمكنك استخدام طرق مثل get()، post()، assertStatus()، assertSee()، وغيرها، التي تحاكي طلبات HTTP وتحقق من النتائج دون الحاجة لكتابة كود معقد.

في مشروع إدارة المنشورات، كتبت أول اختبار لي كالتالي:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostManagementTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_create_a_post()
    {
        $response = $this->post('/posts', [
            'title' => 'عنوان المنشور',
            'body' => 'محتوى المنشور'
        ]);

        $response->assertStatus(302); // تحقق من إعادة التوجيه بعد الحفظ
        $this->assertDatabaseHas('posts', [
            'title' => 'عنوان المنشور'
        ]);
    }
}

لاحظ كيف أن الكود بسيط، واضح، ويغطي سيناريو كامل: إرسال بيانات، التحقق من الاستجابة، والتأكد من أن البيانات حُفظت في قاعدة البيانات. لم أكتب أي كود لتهيئة قاعدة البيانات — كل ذلك مُدار تلقائيًا بفضل الـ trait RefreshDatabase، الذي يُعيد تهيئة قاعدة البيانات قبل كل اختبار.

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

كيف غيّر الـ Testing طريقة عملي اليومية

بعد أن بدأت أكتب اختبارات بانتظام، لاحظت تغييرات جذرية في طريقة تفكيري وعملي. لم يعد الـ Testing مجرد خطوة تقنية، بل أصبح جزءًا من منهجيتي في تطوير البرمجيات.

الثقة في التعديلات الكبيرة

في أحد المشاريع، احتجت إلى إعادة هيكلة كاملة لنظام الصلاحيات (Authorization). في الماضي، كنت سأقضي يومًا كاملاً في التعديل، ثم أبدأ باختبار كل وظيفة يدويًا، وأتمنى أن لا أكون قد نسيت شيئًا. لكن هذه المرة، كان لديّ بالفعل مجموعة من الاختبارات التي تغطي سيناريوهات مختلفة: مستخدم عادي يحاول حذف منشور، مدير يحاول تعديل إعدادات، إلخ.

بعد إجراء التعديلات، شغّلت جميع الاختبارات بسطر واحد:

php artisan test

وظهرت النتيجة خلال ثوانٍ: كل شيء يعمل كما هو متوقع. لو كان هناك خلل، لعرفت مكانه بالضبط، دون الحاجة لفتح المتصفح والتنقل بين الصفحات. هذه الثقة غيرت طريقة تفكيري: أصبحت أجرؤ على إعادة كتابة أجزاء كاملة من الكود، لأنني أعلم أن الاختبارات ستُنبّهني فورًا إذا كسرت شيئًا.

اكتشاف الأخطاء قبل أن تصل للمستخدم

في مشروع آخر، كنت أعمل على نظام فواتير. كتبت دالة تحسب الضريبة بناءً على الدولة. في البداية، اختبرتها يدويًا مع دولتين، وعملت بشكل جيد. لكن بعد أسبوع، أبلغني عميل أن الضريبة تُحسب خطأ لدولة معينة. تبين أنني نسيت حالة خاصة لتلك الدولة.

بعد إصلاح الخطأ، كتبت اختبارًا يغطي هذه الحالة:

public function test_tax_calculation_for_saudi_arabia()
{
    $invoice = new Invoice(['country' => 'SA', 'amount' => 1000]);
    $this->assertEquals(150, $invoice->calculateTax()); // 15% ضريبة
}

منذ ذلك اليوم، لم يعد هذا الخطأ يظهر أبدًا. حتى لو عدّلت في منطق الحساب لاحقًا، سيُنبّهني الاختبار فورًا إذا أثرت على هذه الحالة. هذا النوع من الحماية لا يمكن تحقيقه بالاختبار اليدوي.

توثيق حي للكود

أحد الجوانب التي لم أتوقعها هو أن الاختبارات أصبحت نوعًا من "التوثيق الحي" للمشروع. عندما ينضم مطور جديد للفريق، لا يحتاج لقراءة مستندات طويلة. يكفي أن يقرأ ملفات الاختبارات ليعرف: ما المدخلات المتوقعة؟ ما المخرجات؟ ما السلوكيات المقبولة وغير المقبولة؟

على سبيل المثال، إذا رأى مطور جديد اختبارًا مثل:

public function test_user_cannot_delete_post_if_not_owner()
{
    // إعداد مستخدمين ومنشور
    $this->actingAs($nonOwnerUser);
    $response = $this->delete("/posts/{$post->id}");
    $response->assertStatus(403);
}

فسيفهم فورًا أن النظام يمنع حذف المنشورات من قبل غير المالك، وأنه يُرجع حالة 403 في هذه الحالة. هذا أوضح بكثير من وصف نصي في ملف README.

نصيحة: اكتب أسماء اختباراتك كجمل كاملة تصف السلوك. مثلاً: test_user_can_login_with_valid_credentials أفضل من test_login.

أنواع الاختبارات في Laravel: متى تستخدم كل نوع؟

في Laravel، هناك نوعان رئيسيان من الاختبارات: Unit Tests و Feature Tests. فهم الفرق بينهما سيساعدك على كتابة اختبارات فعّالة دون إهدار الوقت.

Unit Tests: اختبار الوحدات الصغيرة بمعزل

الـ Unit Tests تُستخدم لاختبار وظائف أو كلاسات فردية، دون الاعتماد على مكونات خارجية مثل قاعدة البيانات أو الشبكة. الهدف هو التأكد أن المنطق الداخلي للكود يعمل كما هو متوقع.

مثال: لديك كلاس PriceCalculator يحسب السعر النهائي بعد الخصم والضريبة. يمكنك كتابة Unit Test له كالتالي:

<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\Services\PriceCalculator;

class PriceCalculatorTest extends TestCase
{
    public function test_final_price_with_discount_and_tax()
    {
        $calculator = new PriceCalculator();
        $finalPrice = $calculator->calculate(100, 10, 15); // سعر، خصم %، ضريبة %
        $this->assertEquals(103.5, $finalPrice);
    }
}

هذا الاختبار لا يحتاج لقاعدة بيانات، ولا لطلب HTTP. هو بسيط، سريع، ودقيق. استخدم Unit Tests عندما تريد اختبار منطق رياضي، تحويلات بيانات، أو أي وظيفة معزولة.

Feature Tests: اختبار سلوك النظام ككل

الـ Feature Tests (أو Integration Tests) تُحاكي تفاعل المستخدم مع النظام. تشمل طلبات HTTP، قاعدة البيانات، الجلسات، والـ middleware. هي الأقرب لتجربة المستخدم الفعلية.

مثال: اختبار عملية التسجيل الكاملة:

public function test_user_can_register()
{
    $response = $this->post('/register', [
        'name' => 'مستخدم جديد',
        'email' => 'user@example.com',
        'password' => 'password123',
        'password_confirmation' => 'password123'
    ]);

    $response->assertRedirect('/dashboard');
    $this->assertDatabaseHas('users', ['email' => 'user@example.com']);
    $this->assertAuthenticated();
}

هذا الاختبار يغطي عدة طبقات: التحقق من صحة البيانات، إنشاء المستخدم، تسجيل الدخول التلقائي، وإعادة التوجيه. هذا النوع من الاختبارات ضروري لضمان أن مكونات النظام تعمل معًا بشكل صحيح.

تذكير: لا تحاول تغطية كل شيء بـ Unit Tests. ركّز على المنطق المعقد. أما سلوك المستخدم، فغطّه بـ Feature Tests.

أخطاء شائعة واجهتها (وكيف تجنبتها)

رغم أن Laravel يجعل الـ Testing أسهل، إلا أنني وقعت في أخطاء كثيرة في البداية. مشاركتها قد توفر لك وقتًا ثمينًا.

الإفراط في التفاصيل في الاختبارات

في أول مشروع كتبت له اختبارات، كنت أتحقق من كل سطر HTML في الاستجابة! كتبت أشياء مثل:

$response->assertSee('<h1>مرحباً بك</h1>');

المشكلة أن أي تعديل بسيط في الواجهة (مثل تغيير "مرحباً بك" إلى "أهلاً بك") سيُفشل الاختبار، رغم أن المنطق لا يزال سليمًا. تعلّمت لاحقًا أن أركز على السلوك، وليس على المحتوى الدقيق. الآن، أتحقق فقط من العناصر الأساسية التي تدل على نجاح العملية، مثل وجود رسالة نجاح أو وجود سجل في قاعدة البيانات.

تجاهل تهيئة الحالة (Setup)

في البداية، كنت أكرر نفس الكود في كل اختبار لإنشاء مستخدم أو منشور. هذا جعل الكود طويلًا وصعب الصيانة. الحل كان استخدام طرق مثل setUp() أو الـ factories.

مثال باستخدام Factory:

public function test_admin_can_delete_any_post()
{
    $admin = User::factory()->create(['role' => 'admin']);
    $post = Post::factory()->create();

    $this->actingAs($admin)
         ->delete("/posts/{$post->id}")
         ->assertStatus(302);

    $this->assertDatabaseMissing('posts', ['id' => $post->id]);
}

بفضل الـ factories، إنشاء بيانات اختبار أصبح سهلًا ومتناسقًا.

الاعتماد الكلي على الاختبارات دون فهمها

مرة، كتبت اختبارًا ونجح، فظننت أن كل شيء على ما يرام. لكن لاحقًا اكتشفت أن الاختبار كان "ينجح دائمًا" بسبب خطأ في المنطق. تعلّمت أن أقرأ نتائج الاختبار بعناية، وأتأكد أن الفشل يحدث عندما يجب أن يفشل. جرب أحيانًا أن تُدخل خطأ متعمدًا في الكود وتأكد أن الاختبار يفشل فعلاً.

أدوات وأساليب رفعت من كفاءتي في الـ Testing

مع الوقت، اكتشفت أدوات وأساليب جعلت كتابة الاختبارات أسرع وأكثر فاعلية.

PHPUnit و Artisan Test

Laravel يأتي مدمجًا مع PHPUnit، لكنه يضيف طبقة تجريد تسهّل الاستخدام. الأمر php artisan test لا يعرض فقط النجاح أو الفشل، بل يعطيك تفاصيل جميلة بالألوان، ووقت تنفيذ كل اختبار، وعدد الاختبارات الناجحة والفاشلة.

يمكنك أيضًا تشغيل اختبار واحد فقط:

php artisan test --filter=test_user_can_register

أو تشغيل جميع اختبارات مجلد معين:

php artisan test tests/Feature/Auth

Database Transactions و RefreshDatabase

كما ذكرت سابقًا، الـ trait RefreshDatabase يُعيد تهيئة قاعدة البيانات قبل كل اختبار. لكن في بعض الحالات، يكون هذا بطيئًا. الحل البديل هو استخدام DatabaseTransactions، الذي يلغي التغييرات بعد كل اختبار عبر rollback، دون الحاجة لإعادة إنشاء الجداول.

use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use DatabaseTransactions;
    // باقي الكود
}

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

في مشروع تكامل مع خدمة دفع خارجية (مثل Stripe)، لا تريد أن ترسل طلبات حقيقية في كل اختبار. هنا يأتي دور الـ Mocking.

public function test_payment_success_redirects_to_success_page()
{
    $this->mock(PaymentService::class, function ($mock) {
        $mock->shouldReceive('process')->andReturn(true);
    });

    $response = $this->post('/pay', ['amount' => 100]);
    $response->assertRedirect('/payment/success');
}

بهذا الشكل، تحاكي نجاح عملية الدفع دون الاتصال بالخدمة الحقيقية.

تجربتي العملية: في مشروع تجارة إلكترونية، وفّر لي الـ Mocking ساعات من الانتظار، لأنني لم أعد أحتاج لانتظار استجابة الخوادم الخارجية في كل مرة أشغّل فيها الاختبارات.

كيف تبدأ رحلتك مع الـ Testing في Laravel؟

إذا كنت لا تزال مترددًا، فابدأ بخطوات صغيرة. لا تحاول تغطية مشروعك بالكامل من اليوم الأول.

ابدأ باختبارات الميزات الحرجة

اختر جزءًا من نظامك لا يمكن أن ينكسر أبدًا — مثل تسجيل الدخول، الدفع، أو حذف الحساب. اكتب اختبارًا بسيطًا يغطي السيناريو الأساسي. حتى اختبار واحد أفضل من لا شيء.

اجعل الـ Testing جزءًا من سير عملك

لا تكتب الاختبارات بعد الانتهاء من الكود. اجعلها جزءًا من دورة التطوير: فكّر في السلوك المطلوب، اكتب الاختبار، ثم اكتب الكود الذي يجعله ينجح. هذه الممارسة تُعرف بـ Test-Driven Development (TDD)، وهي فعّالة جدًا في Laravel.

استخدم CI/CD لتشغيل الاختبارات تلقائيًا

إذا كنت تستخدم GitHub أو GitLab، يمكنك إعداد Pipeline يشغّل الاختبارات تلقائيًا عند كل push. هذا يضمن أن أي تعديل جديد لا يكسر النظام الحالي.

مثال بسيط لملف .github/workflows/tests.yml:

name: Tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - name: Install dependencies
        run: composer install
      - name: Run tests
        run: php artisan test

الخلاصة: من كره إلى شراكة دائمة

الرحلة من كره الـ Testing إلى تبنيه لم تكن سهلة، لكنها كانت ضرورية. Laravel لم يُجبرني على تغيير نظرتي، بل قدّم لي الأدوات والبيئة التي جعلت هذا التغيير طبيعيًا وسلسًا. اليوم، لا أتخيل أن أبدأ مشروعًا جديدًا دون كتابة اختبارات من اليوم الأول.

الاختبارات لم تعد عبئًا، بل صديقًا يحميني من أخطائي، يوفر لي الوقت، ويعطيني ثقة لا تقدّر بثمن. إذا كنت لا تزال تتجنب الـ Testing، فأنصحك بتجربة بسيطة: اختر ميزة صغيرة في مشروعك الحالي، واكتب لها اختبارًا واحدًا باستخدام Laravel. شغّله، غيّر الكود، وشغّله مرة أخرى. ستشعر بنفس الإحساس الذي جعلني أحب هذه الممارسة.

في النهاية، البرمجة ليست فقط عن جعل الكود يعمل، بل عن جعله يعمل بشكل صحيح، ويظل يعمل مع مرور الوقت. والـ Testing هو الجسر بين هذين الهدفين. جربه، وستشكر نفسك لاحقًا.

إرسال تعليق

الموافقة على ملفات تعريف الارتباط
”نحن نقدم ملفات تعريف الارتباط على هذا الموقع لتحليل حركة المرور وتذكر تفضيلاتك وتحسين تجربتك.“
لا يتوفر اتصال بالإنترنت!
”يبدو أن هناك خطأ ما في اتصالك بالإنترنت ، يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.“
تم الكشف عن مانع الإعلانات!
”لقد اكتشفنا أنك تستخدم مكونًا إضافيًا لحظر الإعلانات في متصفحك.
تُستخدم العائدات التي نحققها من الإعلانات لإدارة موقع الويب هذا ، ونطلب منك إدراج موقعنا في القائمة البيضاء في المكون الإضافي لحظر الإعلانات.“
Site is Blocked
Sorry! This site is not available in your country.