Laravel المتقدم

الاختبار المتقدم: اختبارات المتصفح والتكامل

18 دقيقة الدرس 20 من 40

الاختبار المتقدم: اختبارات المتصفح والتكامل

بالإضافة إلى اختبارات الوحدة، تتضمن استراتيجيات الاختبار الشاملة أتمتة المتصفح للاختبار من البداية إلى النهاية واختبارات التكامل التي تتحقق من عمل مكونات متعددة معاً. يوفر Laravel Dusk أتمتة متصفح تعبيرية، ويقدم Pest PHP بناء جملة اختبار أنيق، وقدرات الاختبار المتوازي تسرع بشكل كبير مجموعات الاختبار. يغطي هذا الدرس اختبار المتصفح باستخدام Dusk والاختبار الحديث باستخدام Pest وتشغيل الاختبارات بشكل متوازٍ وأنماط تكامل CI/CD وقياس تغطية الاختبار.

Laravel Dusk لاختبار المتصفح

يوفر Laravel Dusk واجهة برمجة تطبيقات تعبيرية لأتمتة المتصفح والاختبار. يقود متصفح Chrome أو Firefox حقيقي، مما يسمح لك باختبار التطبيقات الثقيلة بـ JavaScript وتفاعلات المستخدم.

# تثبيت Laravel Dusk
composer require --dev laravel/dusk

# تثبيت Dusk في تطبيقك
php artisan dusk:install

# تشغيل اختبارات Dusk
php artisan dusk

# تشغيل اختبار محدد
php artisan dusk tests/Browser/LoginTest.php

<?php

namespace Tests\Browser;

use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class LoginTest extends DuskTestCase
{
    // التنقل الأساسي والتأكيدات
    public function test_user_can_login()
    {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => bcrypt('password'),
        ]);

        $this->browse(function (Browser $browser) use ($user) {
            $browser->visit('/login')
                ->type('email', $user->email)
                ->type('password', 'password')
                ->press('تسجيل الدخول')
                ->assertPathIs('/dashboard')
                ->assertSee('مرحباً');
        });
    }

    // متصفحات متعددة لاختبار ميزات الوقت الفعلي
    public function test_chat_between_users()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $this->browse(function (Browser $first, Browser $second) use ($user1, $user2) {
            // المستخدم الأول يسجل الدخول ويرسل رسالة
            $first->loginAs($user1)
                ->visit('/chat')
                ->type('#message-input', 'مرحباً من المستخدم 1')
                ->press('#send-button')
                ->assertSee('مرحباً من المستخدم 1');

            // المستخدم الثاني يسجل الدخول ويرى الرسالة
            $second->loginAs($user2)
                ->visit('/chat')
                ->waitForText('مرحباً من المستخدم 1', 5)
                ->assertSee('مرحباً من المستخدم 1')
                ->type('#message-input', 'مرحباً بك!')
                ->press('#send-button');

            // المستخدم الأول يرى الرد
            $first->waitForText('مرحباً بك!', 5)
                ->assertSee('مرحباً بك!');
        });
    }

    // الانتظار للعناصر و JavaScript
    public function test_dynamic_content_loading()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/products')
                ->waitFor('#product-list', 10) // انتظر حتى 10 ثواني
                ->assertVisible('#product-list')
                ->waitForText('اسم المنتج')
                ->click('#load-more')
                ->pause(1000) // توقف لمدة ثانية واحدة
                ->waitUntilMissing('.loading-spinner')
                ->assertSeeIn('#product-count', '20 منتجاً');
        });
    }

    // تفاعلات النماذج
    public function test_create_product_with_validation()
    {
        $user = User::factory()->create();

        $this->browse(function (Browser $browser) use ($user) {
            $browser->loginAs($user)
                ->visit('/products/create')
                // إرسال نموذج فارغ لتشغيل التحقق
                ->press('إنشاء منتج')
                ->assertSee('حقل الاسم مطلوب')
                ->assertSee('حقل السعر مطلوب')
                // ملء النموذج بشكل صحيح
                ->type('name', 'منتج تجريبي')
                ->type('price', '99.99')
                ->select('category', 'electronics')
                ->attach('image', __DIR__ . '/test-image.jpg')
                ->check('is_featured')
                ->press('إنشاء منتج')
                ->assertPathIs('/products')
                ->assertSee('تم إنشاء المنتج بنجاح')
                ->assertSee('منتج تجريبي');
        });
    }

    // تنفيذ وتقييم JavaScript
    public function test_javascript_interactions()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/calculator')
                ->click('#button-2')
                ->click('#button-plus')
                ->click('#button-3')
                ->click('#button-equals')
                ->assertScript('document.querySelector("#display").value === "5"')
                ->script('window.scrollTo(0, 500)')
                ->assertScript('window.pageYOffset >= 500');
        });
    }

    // تنزيلات الملفات
    public function test_download_report()
    {
        $user = User::factory()->create();

        $this->browse(function (Browser $browser) use ($user) {
            $browser->loginAs($user)
                ->visit('/reports')
                ->click('#download-csv')
                ->pause(2000);

            $downloads = glob(storage_path('app/downloads/*.csv'));
            $this->assertNotEmpty($downloads);
            $this->assertStringContainsString('report', end($downloads));
        });
    }

    // التقاط لقطات الشاشة للتصحيح
    public function test_with_screenshots()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/complex-page')
                ->screenshot('initial-state')
                ->click('#trigger-modal')
                ->screenshot('modal-opened')
                ->assertVisible('#modal');
        });

        // لقطات الشاشة محفوظة في tests/Browser/screenshots/
    }

    // تغيير حجم النافذة للاختبار المتجاوب
    public function test_mobile_navigation()
    {
        $this->browse(function (Browser $browser) {
            $browser->resize(375, 667) // أبعاد iPhone
                ->visit('/')
                ->assertMissing('#desktop-nav')
                ->assertVisible('#mobile-nav')
                ->click('#hamburger-menu')
                ->assertVisible('#mobile-menu')
                ->clickLink('حول')
                ->assertPathIs('/about');
        });
    }

    // مكونات الصفحة (محددات قابلة لإعادة الاستخدام)
    public function test_using_page_components()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit(new Pages\Dashboard)
                ->assertSee('لوحة المعلومات')
                ->within(new Components\Navigation, function ($browser) {
                    $browser->clickLink('الإعدادات');
                })
                ->on(new Pages\Settings)
                ->assertSee('الإعدادات');
        });
    }

    protected function tearDown(): void
    {
        // تنظيف بيانات الاختبار
        foreach (static::$browsers as $browser) {
            $browser->quit();
        }

        parent::tearDown();
    }
}

// فئة صفحة Dusk لمنطق الصفحة القابل لإعادة الاستخدام
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;

class Dashboard extends Page
{
    public function url()
    {
        return '/dashboard';
    }

    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url())
            ->assertSee('لوحة المعلومات');
    }

    public function elements()
    {
        return [
            '@stats-widget' => '#stats-widget',
            '@recent-orders' => '#recent-orders',
            '@logout-button' => 'button[name=logout]',
        ];
    }

    public function viewRecentOrders(Browser $browser)
    {
        $browser->click('@recent-orders');
    }
}
أفضل ممارسة: استخدم Dusk لتدفقات المستخدم الحرجة (التسجيل، الدفع، الدفع) والميزات التي تعتمد بشكل كبير على JavaScript. لاختبار API والتفاعلات HTTP البسيطة، استخدم اختبارات الميزات بدلاً من ذلك - فهي أسرع بكثير.

Pest PHP - إطار اختبار حديث

Pest هو إطار اختبار أنيق مبني على PHPUnit مع التركيز على البساطة. يوفر بناء جملة تعبيري ومكونات إضافية قوية وتجربة مطور أفضل.

# تثبيت Pest
composer require pestphp/pest --dev --with-all-dependencies
composer require pestphp/pest-plugin-laravel --dev

# تهيئة Pest
php artisan pest:install

# تشغيل اختبارات Pest
./vendor/bin/pest

# التشغيل مع التغطية
./vendor/bin/pest --coverage

# تشغيل ملف اختبار محدد
./vendor/bin/pest tests/Feature/UserTest.php

<?php

// tests/Feature/UserTest.php - بناء جملة Pest
use App\Models\User;

test('يمكن للمستخدم التسجيل', function () {
    $response = $this->post('/register', [
        'name' => 'أحمد محمد',
        'email' => 'ahmad@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

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

it('يتطلب بريداً إلكترونياً للتسجيل', function () {
    $response = $this->post('/register', [
        'name' => 'أحمد محمد',
        'password' => 'password',
    ]);

    $response->assertSessionHasErrors('email');
});

// استخدام اختبارات من مستوى أعلى
it('ينشئ مستخدمين')
    ->expect(fn() => User::factory()->create())
    ->toBeInstanceOf(User::class)
    ->toHaveKey('email');

// اختبار مجموعة البيانات (موفرات البيانات)
it('يتحقق من تنسيق البريد الإلكتروني', function ($email) {
    $response = $this->post('/register', [
        'name' => 'أحمد',
        'email' => $email,
        'password' => 'password',
    ]);

    $response->assertSessionHasErrors('email');
})->with([
    'بريد-غير-صالح',
    '@example.com',
    'user@',
    'user@.com',
]);

// خطافات قبل وبعد
beforeEach(function () {
    $this->user = User::factory()->create();
});

afterEach(function () {
    // التنظيف
});

// إعداد مشترك عبر الاختبارات
uses(Tests\TestCase::class)->in('Feature', 'Unit');

// تجميع الاختبارات
describe('تسجيل المستخدم', function () {
    beforeEach(function () {
        // إعداد لجميع الاختبارات في هذه المجموعة
    });

    test('تسجيل ناجح', function () {
        // تطبيق الاختبار
    });

    test('التسجيل ببريد إلكتروني موجود', function () {
        // تطبيق الاختبار
    });
});

// توقعات مخصصة
expect($user)
    ->toBeInstanceOf(User::class)
    ->toHaveProperty('email')
    ->email->toBe('ahmad@example.com');

expect($users)
    ->toHaveCount(3)
    ->each->toBeInstanceOf(User::class);

// اختبار الاستثناءات
test('يرمي استثناءً لبيانات غير صالحة', function () {
    $service = new UserService();
    $service->createUser([]); // يجب أن يرمي
})->throws(ValidationException::class);

// اختبار اللقطة
test('يولد HTML الفاتورة الصحيح', function () {
    $invoice = generateInvoice($order);
    expect($invoice)->toMatchSnapshot();
});

// التنفيذ المتوازي (يُغطى في القسم التالي)
test('اختبار متوازٍ 1', function () {
    expect(true)->toBeTrue();
})->group('parallel');

test('اختبار متوازٍ 2', function () {
    expect(true)->toBeTrue();
})->group('parallel');
نصيحة الترحيل: يمكنك تبني Pest تدريجياً إلى جانب PHPUnit. يمكن أن تتعايش اختبارات Pest و PHPUnit في نفس المشروع. ابدأ بالاختبارات الجديدة في Pest مع الاحتفاظ باختبارات PHPUnit الموجودة.

الاختبار المتوازي

تشغيل الاختبارات بشكل متوازٍ يمكن أن يقلل بشكل كبير من وقت تنفيذ مجموعة الاختبار. يدعم Laravel الاختبار المتوازي خارج الصندوق باستخدام علامة --parallel.

# تشغيل الاختبارات بشكل متوازٍ (PHPUnit)
php artisan test --parallel

# التشغيل بعدد محدد من العمليات
php artisan test --parallel --processes=4

# تشغيل اختبارات Pest بشكل متوازٍ
./vendor/bin/pest --parallel

# التكوين في phpunit.xml
<phpunit>
    <extensions>
        <extension class="ParaTest\Extension"/>
    </extensions>
</phpunit>

<?php

// tests/Pest.php - تكوين التنفيذ المتوازي
uses(Tests\TestCase::class)
    ->beforeEach(function () {
        // كل عملية متوازية تحصل على قاعدة بيانات معزولة
        if (ParallelTesting::running()) {
            $this->artisan('migrate:fresh');
        }
    })
    ->in('Feature');

// تكوين قاعدة البيانات للاختبار المتوازي
// config/database.php
'connections' => [
    'mysql' => [
        'driver' => 'mysql',
        'database' => env(
            'DB_DATABASE',
            'laravel_' . (ParallelTesting::token() ?? '')
        ),
    ],
],

// التعامل مع الموارد المشتركة في الاختبارات المتوازية
use Illuminate\Support\Facades\ParallelTesting;

test('يتعامل مع الوصول المتوازي لقاعدة البيانات', function () {
    // كل عملية تحصل على رمز فريد
    $token = ParallelTesting::token(); // يُرجع 1، 2، 3، إلخ.

    // إنشاء موارد فريدة
    Storage::disk('testing')->put("file_{$token}.txt", 'محتوى');

    // التأكيدات
    expect(Storage::disk('testing')->exists("file_{$token}.txt"))->toBeTrue();
});

// الإعداد والتنظيف للاختبار المتوازي
// tests/ParallelTestCase.php
namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\ParallelTesting;

abstract class ParallelTestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        if (ParallelTesting::running()) {
            // عزل كل عملية
            config(['cache.default' => 'array']);
            config(['session.driver' => 'array']);
        }
    }

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();

        if (ParallelTesting::running()) {
            // تشغيل مرة واحدة لكل عملية متوازية
            $token = ParallelTesting::token();
            exec("php artisan migrate:fresh --database=testing_{$token}");
        }
    }
}

// منع التنفيذ المتوازي لاختبارات محددة
test('اختبار تسلسلي', function () {
    // اختبار لا يمكن تشغيله بشكل متوازٍ
})->group('sequential');

// تشغيل الاختبارات التسلسلية بشكل منفصل
./vendor/bin/pest --exclude-group=parallel
./vendor/bin/pest --group=parallel --parallel
تحذير: يتطلب الاختبار المتوازي معالجة دقيقة للموارد المشتركة (الملفات، ذاكرة التخزين المؤقت، واجهات برمجة التطبيقات الخارجية). تأكد دائماً من أن الاختبارات معزولة ولا تعتمد على ترتيب التنفيذ أو الحالة المشتركة.

تكامل CI/CD

دمج الاختبارات في خطوط أنابيب CI/CD يضمن جودة الكود ويلتقط الأخطاء قبل النشر. إليك أمثلة لمنصات CI الشائعة.

# GitHub Actions - .github/workflows/tests.yml
name: Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  tests:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

      redis:
        image: redis:alpine
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, dom, fileinfo, mysql
          coverage: xdebug

      - name: Copy .env
        run: php -r "file_exists('.env') || copy('.env.example', '.env');"

      - name: Install Dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Generate key
        run: php artisan key:generate

      - name: Directory Permissions
        run: chmod -R 777 storage bootstrap/cache

      - name: Run Migration
        run: php artisan migrate --force
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password

      - name: Run Tests
        run: php artisan test --parallel --coverage --min=80
        env:
          DB_CONNECTION: mysql

      - name: Upload Coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.xml

# GitLab CI - .gitlab-ci.yml
image: php:8.2

stages:
  - test

cache:
  paths:
    - vendor/

before_script:
  - apt-get update -yqq
  - apt-get install -yqq git libzip-dev
  - docker-php-ext-install pdo_mysql zip
  - curl -sS https://getcomposer.org/installer | php
  - php composer.phar install

test:
  stage: test
  services:
    - mysql:8.0
  variables:
    MYSQL_ROOT_PASSWORD: password
    MYSQL_DATABASE: testing
    DB_HOST: mysql
    DB_USERNAME: root
    DB_PASSWORD: password
  script:
    - php artisan migrate --force
    - php artisan test --parallel
  coverage: '/^\s*Lines:\s*\d+.\d+\%/'

# CircleCI - .circleci/config.yml
version: 2.1

jobs:
  test:
    docker:
      - image: cimg/php:8.2
      - image: cimg/mysql:8.0
        environment:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing

    steps:
      - checkout

      - run:
          name: Install PHP Extensions
          command: sudo docker-php-ext-install pdo_mysql

      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "composer.lock" }}

      - run:
          name: Install Dependencies
          command: composer install -n --prefer-dist

      - save_cache:
          key: v1-dependencies-{{ checksum "composer.lock" }}
          paths:
            - vendor

      - run:
          name: Run Tests
          command: php artisan test --parallel

workflows:
  version: 2
  test:
    jobs:
      - test

تغطية الاختبار

قياس تغطية الاختبار يساعد في تحديد مسارات الكود غير المختبرة ويضمن مجموعات اختبار شاملة.

# توليد تقرير التغطية باستخدام PHPUnit
php artisan test --coverage

# توليد تقرير تغطية HTML
php artisan test --coverage-html coverage-report

# توليد التغطية مع حد أدنى
php artisan test --coverage --min=80

# تغطية Pest
./vendor/bin/pest --coverage --min=80

# تكوين التغطية في phpunit.xml
<phpunit>
    <coverage>
        <include>
            <directory suffix=".php">./app</directory>
        </include>
        <exclude>
            <directory>./app/Console</directory>
            <file>./app/Providers/RouteServiceProvider.php</file>
        </exclude>
        <report>
            <html outputDirectory="coverage-report"/>
            <clover outputFile="coverage.xml"/>
        </report>
    </coverage>
</phpunit>

# تفسير التغطية
# - Lines: نسبة أسطر الكود المنفذة
# - Functions: نسبة الوظائف/الطرق المستدعاة
# - Classes: نسبة الفئات المثبتة
# - Branches: نسبة الفروع الشرطية المختبرة

# أهداف التغطية المثالية:
# - منطق الأعمال الحرج: 90-100%
# - Controllers/Services: 80-90%
# - Models: 70-80%
# - المشروع الإجمالي: 70-80%

# التكامل مع أدوات جودة الكود
# composer.json
{
    "scripts": {
        "test": "pest",
        "test:coverage": "pest --coverage --min=80",
        "test:parallel": "pest --parallel",
        "analyse": "phpstan analyse",
        "format": "php-cs-fixer fix"
    }
}

# خطاف ما قبل الالتزام لتغطية الاختبار
# .git/hooks/pre-commit
#!/bin/sh

echo "تشغيل الاختبارات مع التغطية..."
php artisan test --coverage --min=80

if [ $? -ne 0 ]; then
    echo "فشلت الاختبارات أو التغطية أقل من الحد الأدنى. تم إحباط الالتزام."
    exit 1
fi

تمرين 1: اختبار المتصفح باستخدام Dusk

أنشئ اختبارات متصفح شاملة لتدفق عربة التسوق:

  1. اختبر إضافة المنتجات إلى العربة (انقر على "إضافة إلى العربة"، تحقق من تحديث العدد)
  2. اختبر إزالة العناصر من العربة (انقر على أيقونة الإزالة، تحقق من اختفاء العنصر)
  3. اختبر تدفق الدفع (املأ نموذج الشحن، اختر طريقة الدفع، أكد الطلب)
  4. استخدم متصفحات متعددة لاختبار عمليات العربة المتزامنة
  5. التقط لقطات الشاشة في الخطوات الرئيسية للتصحيح
  6. اختبر التصميم المتجاوب بتغيير حجم المتصفح إلى أبعاد الهاتف المحمول

تمرين 2: ترحيل الاختبارات إلى Pest PHP

حوّل اختبارات PHPUnit الحالية إلى Pest وأضف ميزات جديدة:

  1. حوّل 3 فئات اختبار PHPUnit موجودة إلى بناء جملة Pest
  2. أنشئ اختبارات مجموعة البيانات لسيناريوهات التحقق (اختبر 10+ رسائل بريد إلكتروني غير صالحة)
  3. استخدم توقعات من مستوى أعلى لتأكيدات النموذج
  4. جمّع الاختبارات ذات الصلة باستخدام كتل describe()
  5. أضف اختبار اللقطة لقوالب البريد الإلكتروني
  6. شغّل الاختبارات وتحقق من نجاح الجميع

تمرين 3: خط أنابيب CI/CD مع التغطية

أعد خط أنابيب CI/CD كاملاً مع تقارير تغطية الاختبار:

  1. أنشئ سير عمل GitHub Actions يشغّل الاختبارات عند الدفع و PR
  2. قم بتكوين الاختبار المتوازي بـ 4 عمليات
  3. أعد خدمات MySQL و Redis في سير العمل
  4. ولّد تقرير تغطية الاختبار وفرض حد أدنى 80%
  5. ارفع التغطية إلى Codecov أو خدمة مماثلة
  6. أضف شارة الحالة إلى README تظهر حالة الاختبار والتغطية
  7. اختبر سير العمل بدفع التزام

الخلاصة

في هذا الدرس، أتقنت تقنيات الاختبار المتقدمة لضمان الجودة الشامل. تعلمت أتمتة المتصفح باستخدام Laravel Dusk للاختبار من البداية إلى النهاية للتطبيقات الثقيلة بـ JavaScript والتفاعلات المعقدة للمستخدمين، واستكشفت Pest PHP لبناء جملة اختبار أنيق وحديث مع مجموعات البيانات والتوقعات من مستوى أعلى، ونفذت الاختبار المتوازي لتقليل وقت تنفيذ مجموعة الاختبار بشكل كبير، ودمجت الاختبارات في خطوط أنابيب CI/CD باستخدام GitHub Actions و GitLab CI و CircleCI، وقست تغطية الاختبار لتحديد مسارات الكود غير المختبرة وضمان مجموعات اختبار شاملة. تمكّنك استراتيجيات الاختبار المتقدمة هذه من بناء تطبيقات Laravel قوية ومختبرة جيداً مع الثقة في جودة الكود والموثوقية.