الاختبارات و TDD
إطار عمل Pest PHP
إطار عمل Pest PHP
Pest هو إطار عمل حديث لاختبار PHP مع التركيز على البساطة والصيغة الأنيقة. مبني على PHPUnit، يوفر واجهة برمجة تطبيقات أكثر تعبيراً وسهولة للمطور مع الحفاظ على التوافق الكامل مع اختبارات PHPUnit.
لماذا Pest؟
يقدم Pest العديد من المزايا مقارنة بـ PHPUnit التقليدي:
- صيغة جميلة: كود اختبار نظيف وقابل للقراءة بدون كود نموذجي
- واجهة برمجة التوقعات: تأكيدات معبرة تقرأ مثل اللغة الطبيعية
- اختبار عالي المستوى: اختصارات قوية لأنماط الاختبار الشائعة
- الإضافات: نظام بيئي غني لـ Laravel و Livewire والاختبار المتوازي والمزيد
- مثل TypeScript: مستوحى من Jest، مما يجعله مألوفًا لمطوري JavaScript
- بدون تكوين: يعمل مباشرة مع الإعدادات الافتراضية المعقولة
التثبيت
# تثبيت Pest
composer require pestphp/pest --dev --with-all-dependencies
# تثبيت Pest لـ Laravel
composer require pestphp/pest-plugin-laravel --dev
# تهيئة Pest
./vendor/bin/pest --init
# تشغيل الاختبارات
./vendor/bin/pest
بعد التثبيت: ينشئ Pest ملف
Pest.php في دليل tests الخاص بك للتكوين والإعداد العام.
صيغة Pest الأساسية
اختبارك الأول في Pest
<?php
// tests/Unit/ExampleTest.php
test('basic math works', function () {
expect(2 + 2)->toBe(4);
});
it('can calculate total price', function () {
$price = 100;
$tax = 15;
$total = $price + $tax;
expect($total)->toBe(115);
});
test() مقابل it(): كلا الدالتين تنشئان اختبارات. استخدم
test() للعبارات ("الرياضيات الأساسية تعمل") أو it() للسلوكيات ("يمكن حساب السعر الإجمالي"). اختر بناءً على القراءة.
مقارنة PHPUnit مقابل Pest
<?php
// أسلوب PHPUnit
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created()
{
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('John Doe', $user->name);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com'
]);
}
}
// أسلوب Pest
use function Pest\Laravel\{assertDatabaseHas};
uses(RefreshDatabase::class);
it('can create a user', function () {
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
expect($user)
->toBeInstanceOf(User::class)
->name->toBe('John Doe');
assertDatabaseHas('users', ['email' => 'john@example.com']);
});
واجهة برمجة التوقعات
توفر توقعات Pest واجهة برمجة تطبيقات معبرة وقابلة للربط للتأكيدات:
التوقعات الأساسية
<?php
test('basic expectations', function () {
// المساواة
expect(10)->toBe(10);
expect('hello')->toEqual('hello');
// فحوصات منطقية
expect(true)->toBeTrue();
expect(false)->toBeFalse();
expect(null)->toBeNull();
expect('value')->not->toBeNull();
// فحوصات النوع
expect(100)->toBeInt();
expect('string')->toBeString();
expect([])->toBeArray();
expect(new User)->toBeObject();
// المجموعات
expect([1, 2, 3])->toHaveCount(3);
expect(['a', 'b'])->toContain('a');
expect(['key' => 'value'])->toHaveKey('key');
// المقارنات الرقمية
expect(10)->toBeGreaterThan(5);
expect(5)->toBeLessThan(10);
expect(10)->toBeGreaterThanOrEqual(10);
});
توقعات السلاسل النصية
<?php
test('string expectations', function () {
$email = 'test@example.com';
expect($email)
->toBeString()
->toContain('@')
->toStartWith('test')
->toEndWith('.com')
->toMatch('/^[\w.]+@[\w.]+$/');
expect('HELLO')
->toBeUppercase()
->not->toBeLowercase();
$json = '{"name":"John"}';
expect($json)->toBeJson();
});
توقعات الكائنات والنسخ
<?php
test('object expectations', function () {
$user = new User(['name' => 'John']);
expect($user)
->toBeInstanceOf(User::class)
->toHaveProperty('name')
->name->toBe('John');
$collection = collect([1, 2, 3]);
expect($collection)
->toBeInstanceOf(Collection::class)
->toHaveMethod('map')
->count()->toBe(3);
});
توقعات الاستثناءات
<?php
test('exception expectations', function () {
expect(fn() => throw new Exception('Error'))
->toThrow(Exception::class);
expect(fn() => throw new Exception('Not found'))
->toThrow(Exception::class, 'Not found');
expect(fn() => User::findOrFail(999))
->toThrow(ModelNotFoundException::class);
});
مجموعات البيانات
تسمح لك مجموعات البيانات بتشغيل نفس الاختبار بقيم إدخال مختلفة، مشابهة لموفري البيانات في PHPUnit ولكن بشكل أكثر أناقة:
مجموعات البيانات الأساسية
<?php
it('validates email addresses', function ($email, $isValid) {
$validator = new EmailValidator();
expect($validator->isValid($email))->toBe($isValid);
})->with([
['test@example.com', true],
['invalid-email', false],
['missing@domain', false],
['spaces in@email.com', false],
['valid.email+tag@example.co.uk', true],
]);
مجموعات البيانات المسماة
<?php
it('calculates discount correctly', function ($price, $discount, $expected) {
$calculator = new PriceCalculator();
$result = $calculator->applyDiscount($price, $discount);
expect($result)->toBe($expected);
})->with([
'10% off $100' => [100, 0.10, 90],
'25% off $200' => [200, 0.25, 150],
'50% off $50' => [50, 0.50, 25],
'no discount' => [100, 0, 100],
]);
مجموعات البيانات المشتركة
<?php
// tests/Datasets/Emails.php
dataset('emails', [
'valid gmail' => 'user@gmail.com',
'valid corporate' => 'john.doe@company.com',
'invalid' => 'not-an-email',
]);
// الاستخدام في أي اختبار
it('processes email', function ($email) {
// منطق الاختبار
})->with('emails');
مجموعات البيانات المجمعة
<?php
dataset('users', [
'admin' => User::factory()->admin()->make(),
'regular' => User::factory()->make(),
]);
dataset('actions', ['create', 'edit', 'delete']);
it('checks permissions', function ($user, $action) {
expect($user->can($action))->toBeBool();
})->with('users', 'actions');
// هذا يشغل 6 اختبارات: admin+create، admin+edit، admin+delete، regular+create، إلخ.
الاختبار عالي المستوى
يتيح لك اختبار Pest عالي المستوى اختبار أنماط Laravel الشائعة دون كتابة كود نموذجي:
توقعات عالية المستوى
<?php
// اختبار أن المجموعة لها خصائص معينة
it('has users with emails', function () {
$users = User::factory()->count(3)->create();
expect($users)
->each->toBeInstanceOf(User::class)
->each->toHaveProperty('email')
->each->email->not->toBeEmpty();
});
// اختبار تسلسل القيم
it('generates sequential IDs', function () {
$items = collect([1, 2, 3, 4, 5]);
expect($items)
->sequence(
fn($item) => $item->toBe(1),
fn($item) => $item->toBe(2),
fn($item) => $item->toBe(3),
fn($item) => $item->toBe(4),
fn($item) => $item->toBe(5)
);
});
اختبارات عالية المستوى لـ HTTP
<?php
use function Pest\Laravel\{get, post};
it('shows homepage', fn() => get('/')
->assertOk()
->assertSee('Welcome')
);
it('creates user', fn() => post('/users', [
'name' => 'John',
'email' => 'john@example.com'
])
->assertRedirect()
->assertSessionHas('success')
);
إعداد الاختبار والخطافات
الإعداد العام (Pest.php)
<?php
// tests/Pest.php
uses(Tests\TestCase::class)->in('Feature');
uses(RefreshDatabase::class)->in('Feature');
// دوال مساعدة عامة
function createUser(array $attributes = []): User
{
return User::factory()->create($attributes);
}
// قبل كل اختبار في دليل Feature
beforeEach(function () {
$this->user = createUser();
})->in('Feature');
// بعد كل اختبار
afterEach(function () {
// منطق التنظيف
})->in('Feature');
إعداد كل ملف
<?php
// tests/Feature/OrderTest.php
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->product = Product::factory()->create();
});
it('can create order', function () {
// الوصول إلى $this->user و $this->product
$order = Order::create([
'user_id' => $this->user->id,
'product_id' => $this->product->id
]);
expect($order)->toBeInstanceOf(Order::class);
});
إضافات Pest
إضافة Laravel
<?php
use function Pest\Laravel\{
get, post, put, delete,
actingAs,
assertDatabaseHas,
assertDatabaseMissing
};
it('requires authentication', function () {
get('/dashboard')
->assertRedirect('/login');
$user = User::factory()->create();
actingAs($user)
->get('/dashboard')
->assertOk();
});
it('creates post', function () {
$user = User::factory()->create();
actingAs($user)
->post('/posts', ['title' => 'Test Post'])
->assertCreated();
assertDatabaseHas('posts', ['title' => 'Test Post']);
});
إضافة Faker
<?php
use function Pest\Faker\{faker};
it('generates fake data', function () {
$name = faker()->name;
$email = faker()->email;
$user = User::create([
'name' => $name,
'email' => $email
]);
expect($user->name)->toBe($name);
});
تنظيم الاختبارات
تجميع الاختبارات
<?php
describe('UserController', function () {
beforeEach(function () {
$this->user = User::factory()->create();
});
describe('index', function () {
it('lists all users', function () {
get('/users')->assertOk();
});
it('paginates results', function () {
User::factory()->count(20)->create();
get('/users')
->assertOk()
->assertJsonCount(15, 'data');
});
});
describe('store', function () {
it('creates new user', function () {
post('/users', ['name' => 'John'])
->assertCreated();
});
it('validates required fields', function () {
post('/users', [])
->assertSessionHasErrors(['name', 'email']);
});
});
});
علامات الاختبار
<?php
it('performs critical operation')
->group('critical', 'slow')
->skip('Not ready yet');
it('runs in CI only')
->skipOnWindows()
->skipOnMac();
it('requires API access')
->skipWhen(!env('API_KEY'))
->todo(); // وضع علامة كغير مكتمل
// تشغيل مجموعات معينة
// ./vendor/bin/pest --group=critical
// ./vendor/bin/pest --exclude-group=slow
مثال عملي: اختبار API
<?php
// tests/Feature/Api/ProductApiTest.php
use function Pest\Laravel\{get, post, put, delete, actingAs};
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->token = $this->user->createToken('test')->plainTextToken;
});
describe('Product API', function () {
it('lists all products', function () {
Product::factory()->count(5)->create();
get('/api/products', [
'Authorization' => 'Bearer ' . $this->token
])
->assertOk()
->assertJsonCount(5, 'data')
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'price', 'created_at']
]
]);
});
it('creates product with valid data', function ($name, $price) {
post('/api/products', [
'name' => $name,
'price' => $price
], [
'Authorization' => 'Bearer ' . $this->token
])
->assertCreated()
->assertJson([
'data' => [
'name' => $name,
'price' => $price
]
]);
assertDatabaseHas('products', [
'name' => $name,
'price' => $price
]);
})->with([
['Laptop', 999.99],
['Mouse', 29.99],
['Keyboard', 79.99],
]);
it('validates required fields', function ($field) {
post('/api/products', [], [
'Authorization' => 'Bearer ' . $this->token
])
->assertStatus(422)
->assertJsonValidationErrors([$field]);
})->with(['name', 'price']);
it('requires authentication', function () {
get('/api/products')
->assertUnauthorized();
});
});
تمرين تطبيقي:
قم بتحويل اختبارات PHPUnit هذه إلى Pest:
<?php
class CartTest extends TestCase
{
public function test_can_add_item_to_cart()
{
$cart = new Cart();
$product = new Product(['price' => 100]);
$cart->addItem($product);
$this->assertCount(1, $cart->items());
$this->assertEquals(100, $cart->total());
}
public function test_calculates_total_with_tax()
{
$cart = new Cart();
$cart->addItem(new Product(['price' => 100]));
$total = $cart->totalWithTax(0.15);
$this->assertEquals(115, $total);
}
}
الخلاصة
يقدم Pest تجربة اختبار حديثة وأنيقة مع:
- كود نموذجي أدنى: التركيز على منطق الاختبار، وليس الإعداد
- صيغة معبرة: الاختبارات تقرأ مثل التوثيق
- ميزات قوية: مجموعات البيانات، الاختبار عالي المستوى، الإضافات
- التوافق الكامل: يعمل جنبًا إلى جنب مع اختبارات PHPUnit الموجودة
- تجربة مطور رائعة: إخراج جميل ورسائل خطأ مفيدة
البدء مع Pest: لا يتعين عليك تحويل جميع الاختبارات الموجودة. يعمل Pest جنبًا إلى جنب مع PHPUnit - ابدأ بكتابة اختبارات جديدة مع Pest وقم بالترحيل تدريجيًا عند الحاجة.