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

بناء مجموعة اختبار - الجزء الثاني

15 دقيقة الدرس 34 من 35

بناء مجموعة اختبار - الجزء الثاني

استمرارًا من الجزء الأول، سنضيف الآن اختبارات شاملة، ونقوم بإعداد التكامل المستمر، وإنشاء تقارير التغطية، وتوثيق استراتيجية الاختبار الخاصة بنا.

الاختبارات الشاملة مع Laravel Dusk

إعداد Dusk

# تثبيت Dusk composer require --dev laravel/dusk # تثبيت Dusk في التطبيق php artisan dusk:install # تثبيت ChromeDriver php artisan dusk:chrome-driver # تشغيل اختبارات Dusk php artisan dusk

اختبار تدفق الدفع الكامل

<?php namespace Tests\Browser; use App\Models\User; use App\Models\Product; use Laravel\Dusk\Browser; use Tests\DuskTestCase; use Illuminate\Foundation\Testing\DatabaseMigrations; class CheckoutFlowTest extends DuskTestCase { use DatabaseMigrations; /** @test */ public function user_can_complete_purchase_flow() { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password') ]); $product = Product::factory()->create([ 'name' => 'Test Product', 'price' => 99.99, 'stock' => 10 ]); $this->browse(function (Browser $browser) use ($user, $product) { $browser // الخطوة 1: تصفح المنتجات ->visit('/products') ->assertSee('Test Product') ->assertSee('$99.99') // الخطوة 2: إضافة إلى العربة ->click('@add-to-cart-' . $product->id) ->waitForText('Added to cart') ->assertSee('1') // عدد العربة // الخطوة 3: عرض العربة ->click('@cart-icon') ->assertPathIs('/cart') ->assertSee('Test Product') ->assertSee('$99.99') // الخطوة 4: تسجيل الدخول للدفع ->press('Proceed to Checkout') ->assertPathIs('/login') ->type('email', 'test@example.com') ->type('password', 'password') ->press('Login') // الخطوة 5: ملء معلومات الشحن ->waitForLocation('/checkout') ->type('address', '123 Test Street') ->type('city', 'Test City') ->type('zip', '12345') ->select('country', 'US') // الخطوة 6: إدخال الدفع ->press('Continue to Payment') ->waitForText('Payment Information') ->type('card_number', '4242424242424242') ->type('expiry', '12/25') ->type('cvv', '123') // الخطوة 7: المراجعة وتقديم الطلب ->press('Review Order') ->waitForText('Order Summary') ->assertSee('Test Product') ->assertSee('Subtotal: $99.99') ->assertSee('Tax: $15.00') ->assertSee('Total: $114.99') ->press('Place Order') // الخطوة 8: تأكيد الطلب ->waitForLocation('/orders/confirmation') ->assertSee('Thank you for your order!') ->assertSee('Order #'); // التحقق من الطلب في قاعدة البيانات $this->assertDatabaseHas('orders', [ 'user_id' => $user->id, 'status' => 'paid', 'total' => 114.99 ]); }); } /** @test */ public function checkout_validates_empty_cart() { $user = User::factory()->create(); $this->browse(function (Browser $browser) use ($user) { $browser ->loginAs($user) ->visit('/cart') ->assertSee('Your cart is empty') ->assertMissing('@checkout-button'); }); } /** @test */ public function payment_failure_shows_error() { $user = User::factory()->create(); $product = Product::factory()->create(); $this->browse(function (Browser $browser) use ($user, $product) { // إضافة المنتج والانتقال إلى الدفع $browser ->loginAs($user) ->visit('/products') ->click('@add-to-cart-' . $product->id) ->visit('/checkout') // ... ملء معلومات الشحن ... ->type('card_number', '4000000000000002') // بطاقة سيتم رفضها ->press('Place Order') ->waitForText('Payment declined') ->assertSee('Please try a different card'); // يجب عدم إنشاء الطلب $this->assertDatabaseMissing('orders', [ 'user_id' => $user->id, 'status' => 'paid' ]); }); } }

إعداد التكامل المستمر

سير عمل GitHub Actions

# .github/workflows/tests.yml name: Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: tests: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: test_db ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.2 extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv 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 PHPUnit Tests env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: test_db DB_USERNAME: root DB_PASSWORD: password run: php artisan test --coverage --min=80 - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml fail_ci_if_error: true

تكوين GitLab CI

# .gitlab-ci.yml image: php:8.2 stages: - test - coverage variables: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test_db cache: paths: - vendor/ before_script: - apt-get update -yqq - apt-get install -yqq git libzip-dev unzip - docker-php-ext-install pdo_mysql zip - curl -sS https://getcomposer.org/installer | php - php composer.phar install --no-interaction --prefer-dist test:unit: stage: test script: - php artisan test --testsuite=Unit --coverage artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml test:feature: stage: test services: - mysql:8.0 script: - cp .env.testing .env - php artisan migrate --force - php artisan test --testsuite=Feature test:browser: stage: test before_script: - apt-get install -y chromium chromium-driver script: - php artisan dusk artifacts: when: on_failure paths: - tests/Browser/screenshots/ - tests/Browser/console/ coverage: stage: coverage script: - php artisan test --coverage-html coverage coverage: '/^\s*Lines:\s*\d+.\d+\%/' artifacts: paths: - coverage/

تقارير تغطية الكود

إنشاء التغطية مع PHPUnit

# إنشاء تقرير تغطية HTML php artisan test --coverage-html coverage # إنشاء Clover XML لـ CI php artisan test --coverage-clover clover.xml # تعيين حد أدنى للتغطية php artisan test --min=80 # التغطية لدليل محدد php artisan test --coverage --path=app/Services

قراءة تقارير التغطية

# فتح تقرير HTML open coverage/index.html # مثال على إخراج التغطية: Tests: 145 passed Time: 12.34s Lines: 87.5% ( 875/1000) Methods: 92.3% ( 120/130) Classes: 95.0% ( 19/20)
أهداف التغطية:
  • الكود الحرج (الدفع، المصادقة): تغطية 95%+
  • منطق الأعمال (الخدمات، النماذج): تغطية 85%+
  • المتحكمات: تغطية 75%+
  • المشروع الإجمالي: تغطية 80%+

اختبار الأداء

أداء استعلامات قاعدة البيانات

<?php namespace Tests\Performance; use Tests\TestCase; use App\Models\Product; use Illuminate\Support\Facades\DB; class DatabasePerformanceTest extends TestCase { /** @test */ public function product_listing_uses_efficient_queries() { Product::factory()->count(100)->create(); DB::enableQueryLog(); $products = Product::with('category')->paginate(20); $queries = DB::getQueryLog(); // يجب تنفيذ استعلامين فقط (المنتجات + الفئات) $this->assertLessThanOrEqual(2, count($queries)); } /** @test */ public function order_calculation_completes_quickly() { $order = Order::factory()->create(); OrderItem::factory()->count(50)->create([ 'order_id' => $order->id ]); $start = microtime(true); $total = $order->calculateTotal(); $duration = microtime(true) - $start; // يجب الانتهاء في أقل من 100 مللي ثانية $this->assertLessThan(0.1, $duration); } }

اختبار وقت استجابة API

<?php /** @test */ public function api_endpoints_respond_within_acceptable_time() { $user = User::factory()->create(); $endpoints = [ 'GET /api/products' => 200, // 200 مللي ثانية كحد أقصى 'GET /api/orders' => 300, 'POST /api/cart' => 150, ]; foreach ($endpoints as $endpoint => $maxTime) { [$method, $url] = explode(' ', $endpoint); $start = microtime(true); $this->actingAs($user)->json($method, $url); $duration = (microtime(true) - $start) * 1000; // التحويل إلى مللي ثانية $this->assertLessThan($maxTime, $duration, "{$endpoint} took {$duration}ms (max: {$maxTime}ms)" ); } }

توثيق الاختبار

إنشاء توثيق الاختبار

# docs/testing-guide.md # دليل الاختبار ## نظرة عامة يحتفظ هذا المشروع بمجموعة اختبار شاملة تغطي اختبارات الوحدة والتكامل والاختبارات الشاملة. ## تشغيل الاختبارات ```bash # جميع الاختبارات php artisan test # مجموعة محددة php artisan test --testsuite=Unit php artisan test --testsuite=Feature # ملف محدد php artisan test tests/Unit/Models/ProductTest.php # مع التغطية php artisan test --coverage # اختبارات المتصفح php artisan dusk ``` ## هيكل الاختبار ``` tests/ ├── Unit/ # اختبارات الوحدة المعزولة │ ├── Models/ # اختبارات النموذج │ └── Services/ # اختبارات الخدمة ├── Feature/ # اختبارات HTTP/API │ ├── Auth/ # المصادقة │ └── Api/ # نقاط نهاية API ├── Integration/ # اختبارات متعددة المكونات └── Browser/ # اختبارات شاملة مع Dusk ``` ## كتابة الاختبارات ### اختبارات الوحدة اختبار المكونات الفردية بشكل منفصل: - النماذج (السمات، العلاقات، النطاقات) - الخدمات (منطق الأعمال) - المساعدون (وظائف الأدوات) ### اختبارات الميزات اختبار طلبات واستجابات HTTP: - تدفقات المصادقة - عمليات CRUD - نقاط نهاية API - التحقق ### اختبارات المتصفح اختبار رحلات المستخدم الكاملة: - التسجيل وتسجيل الدخول - تدفق الدفع - تدفقات عمل المسؤول ## متطلبات التغطية - الميزات الحرجة: 95%+ - منطق الأعمال: 85%+ - الإجمالي: 80%+ ## CI/CD تعمل الاختبارات تلقائيًا على: - الدفع إلى main/develop - طلبات السحب - ما قبل النشر تمنع الإخفاقات النشر.

قالب حالة الاختبار

<?php namespace Tests\Unit\Models; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; /** * مجموعة الاختبار: [اسم الميزة] * * الغرض: [ما تغطيه مجموعة الاختبار هذه] * * التغطية: * - [السيناريو 1] * - [السيناريو 2] * * التبعيات: * - [النموذج/الخدمة/إلخ.] */ class ExampleTest extends TestCase { use RefreshDatabase; /** * اختبار: [ما السلوك الذي يتم اختباره] * * معطى: [الحالة/السياق الأولي] * عندما: [الإجراء المنفذ] * ثم: [النتيجة المتوقعة] * * @test * @group feature-name */ public function descriptive_test_name() { // ترتيب // فعل // تأكيد } }

أدوات ومساعدات الاختبار

مساعدات الاختبار المخصصة

<?php namespace Tests; use App\Models\User; use App\Models\Product; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; /** * إنشاء مستخدم مصادق عليه للاختبارات */ protected function actingAsUser(array $attributes = []): User { $user = User::factory()->create($attributes); $this->actingAs($user); return $user; } /** * إنشاء مستخدم مسؤول للاختبارات */ protected function actingAsAdmin(): User { $admin = User::factory()->admin()->create(); $this->actingAs($admin); return $admin; } /** * إضافة منتجات إلى العربة للاختبار */ protected function fillCart(int $productCount = 3): array { $products = Product::factory()->count($productCount)->create(); foreach ($products as $product) { $this->post('/cart', [ 'product_id' => $product->id, 'quantity' => 1 ]); } return $products->toArray(); } /** * التأكيد على تطابق هيكل JSON المتوقع */ protected function assertJsonStructureExact(array $structure, $json): void { $data = is_array($json) ? $json : json_decode($json, true); $this->assertEquals( array_keys($structure), array_keys($data), 'JSON structure does not match expected structure' ); } }

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

تتبع مقاييس الاختبار

# إنشاء تقرير الاختبار php artisan test --log-junit report.xml # أمثلة على المقاييس لتتبعها: - إجمالي عدد الاختبارات - وقت تنفيذ الاختبار - معدل الاختبارات غير المستقرة (الاختبارات التي تفشل بشكل متقطع) - نسبة تغطية الكود - الاختبارات الجديدة المضافة لكل سباق
مشاكل مجموعة الاختبار الشائعة:
  • الاختبارات البطيئة: تستغرق المجموعة وقتًا طويلاً للتشغيل (حسّن أو وازِ)
  • الاختبارات غير المستقرة: الاختبارات تفشل بشكل متقطع (عادة مشاكل التوقيت/غير المتزامن)
  • تغطية منخفضة: مسارات الكود الحرجة غير مختبرة
  • الاختبارات الهشة: تنكسر على تغييرات صغيرة (اختبار التنفيذ وليس السلوك)
  • الاختبارات المكررة: نفس السيناريو مختبر عدة مرات
المشروع النهائي:

أكمل مجموعة اختبار التجارة الإلكترونية بإضافة:

  1. اختبار المتصفح: يمكن للمسؤول إدارة مخزون المنتجات
  2. اختبار API: نقطة نهاية تحديث ملف المستخدم
  3. اختبار التكامل: إرسال إشعار البريد الإلكتروني عند اكتمال الطلب
  4. اختبار الأداء: بحث المنتج ينتهي في أقل من 100 مللي ثانية
  5. CI/CD: إعداد سير عمل GitHub Actions
  6. التغطية: تحقيق تغطية 85%+
  7. التوثيق: اكتب دليل اختبار لفريقك

الخلاصة

أكمل الجزء الثاني مجموعة الاختبار الخاصة بنا مع:

  • اختبارات شاملة: اختبار رحلة المستخدم الكاملة مع Laravel Dusk
  • CI/CD: الاختبار التلقائي على GitHub Actions و GitLab CI
  • تقارير التغطية: إنشاء وتتبع مقاييس تغطية الكود
  • اختبارات الأداء: استعلامات قاعدة البيانات وأوقات استجابة API
  • التوثيق: دليل الاختبار والقوالب للفريق
  • الأدوات: مساعدات مخصصة لتبسيط كتابة الاختبار
صيانة مجموعة الاختبار الخاصة بك:
  • قم بتشغيل الاختبارات قبل كل التزام
  • اكتب اختبارات لكل إصلاح خطأ
  • أعد تصميم الاختبارات عند إعادة تصميم الكود
  • راجع تغطية الاختبار في طلبات السحب
  • حافظ على الاختبارات سريعة (أقل من دقيقتين لمجموعة كاملة)
  • حدّث التوثيق عند تغيير هيكل الاختبار