الاختبارات و TDD
بناء مجموعة اختبار - الجزء الثاني
بناء مجموعة اختبار - الجزء الثاني
استمرارًا من الجزء الأول، سنضيف الآن اختبارات شاملة، ونقوم بإعداد التكامل المستمر، وإنشاء تقارير التغطية، وتوثيق استراتيجية الاختبار الخاصة بنا.
الاختبارات الشاملة مع 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
# أمثلة على المقاييس لتتبعها:
- إجمالي عدد الاختبارات
- وقت تنفيذ الاختبار
- معدل الاختبارات غير المستقرة (الاختبارات التي تفشل بشكل متقطع)
- نسبة تغطية الكود
- الاختبارات الجديدة المضافة لكل سباق
مشاكل مجموعة الاختبار الشائعة:
- الاختبارات البطيئة: تستغرق المجموعة وقتًا طويلاً للتشغيل (حسّن أو وازِ)
- الاختبارات غير المستقرة: الاختبارات تفشل بشكل متقطع (عادة مشاكل التوقيت/غير المتزامن)
- تغطية منخفضة: مسارات الكود الحرجة غير مختبرة
- الاختبارات الهشة: تنكسر على تغييرات صغيرة (اختبار التنفيذ وليس السلوك)
- الاختبارات المكررة: نفس السيناريو مختبر عدة مرات
المشروع النهائي:
أكمل مجموعة اختبار التجارة الإلكترونية بإضافة:
- اختبار المتصفح: يمكن للمسؤول إدارة مخزون المنتجات
- اختبار API: نقطة نهاية تحديث ملف المستخدم
- اختبار التكامل: إرسال إشعار البريد الإلكتروني عند اكتمال الطلب
- اختبار الأداء: بحث المنتج ينتهي في أقل من 100 مللي ثانية
- CI/CD: إعداد سير عمل GitHub Actions
- التغطية: تحقيق تغطية 85%+
- التوثيق: اكتب دليل اختبار لفريقك
الخلاصة
أكمل الجزء الثاني مجموعة الاختبار الخاصة بنا مع:
- اختبارات شاملة: اختبار رحلة المستخدم الكاملة مع Laravel Dusk
- CI/CD: الاختبار التلقائي على GitHub Actions و GitLab CI
- تقارير التغطية: إنشاء وتتبع مقاييس تغطية الكود
- اختبارات الأداء: استعلامات قاعدة البيانات وأوقات استجابة API
- التوثيق: دليل الاختبار والقوالب للفريق
- الأدوات: مساعدات مخصصة لتبسيط كتابة الاختبار
صيانة مجموعة الاختبار الخاصة بك:
- قم بتشغيل الاختبارات قبل كل التزام
- اكتب اختبارات لكل إصلاح خطأ
- أعد تصميم الاختبارات عند إعادة تصميم الكود
- راجع تغطية الاختبار في طلبات السحب
- حافظ على الاختبارات سريعة (أقل من دقيقتين لمجموعة كاملة)
- حدّث التوثيق عند تغيير هيكل الاختبار