الاختبار المتقدم: المحاكاة والتزييف
الاختبار المتقدم: المحاكاة والتزييف
كتابة اختبارات فعالة غالباً ما تتطلب عزل الكود عن التبعيات الخارجية مثل قواعد البيانات وواجهات برمجة التطبيقات وخدمات البريد الإلكتروني وأنظمة الملفات. يوفر Laravel قدرات قوية للمحاكاة والتزييف تسمح لك باختبار منطق التطبيق الخاص بك بشكل مستقل عن هذه الأنظمة الخارجية. يستكشف هذا الدرس محاكاة التبعيات باستخدام Mockery واستخدام التزييفات المدمجة في Laravel للخدمات الشائعة وإنشاء محاكيات جزئية واستخدام الجواسيس وفهم بدائل الاختبار.
فهم بدائل الاختبار
بدائل الاختبار هي كائنات تحل محل التبعيات الحقيقية في الاختبارات. الأنواع المختلفة تخدم أغراضاً مختلفة في استراتيجية الاختبار.
<?php
namespace Tests\Feature;
use Tests\TestCase;
/**
* تسلسل بدائل الاختبار:
*
* 1. Dummy - كائنات يتم تمريرها لكن لا تُستخدم أبداً (تملأ قوائم المعاملات)
* 2. Stub - يوفر إجابات معلبة للاستدعاءات التي تتم أثناء الاختبارات
* 3. Spy - يسجل معلومات حول كيفية استدعائه
* 4. Mock - مبرمج مسبقاً بتوقعات حول الاستدعاءات التي سيستقبلها
* 5. Fake - تطبيق عملي، لكنه غير مناسب للإنتاج (قاعدة بيانات في الذاكرة)
*/
class TestDoublesExamplesTest extends TestCase
{
// DUMMY - لا يُستخدم أبداً، يملأ المعامل فقط
public function test_dummy_example()
{
$dummy = $this->createMock(\App\Services\Logger::class);
// Logger يُمرر لكن لا يُستدعى أبداً
$calculator = new \App\Services\Calculator($dummy);
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
// STUB - يُرجع قيماً محددة مسبقاً
public function test_stub_example()
{
$stub = $this->createMock(\App\Services\ExternalApiClient::class);
// تكوين stub لإرجاع بيانات محددة
$stub->method('fetchUserData')
->willReturn([
'id' => 1,
'name' => 'أحمد محمد',
'email' => 'ahmad@example.com'
]);
$service = new \App\Services\UserService($stub);
$user = $service->getUserFromExternalApi(1);
$this->assertEquals('أحمد محمد', $user['name']);
}
// SPY - يسجل استدعاءات الطرق للتحقق لاحقاً
public function test_spy_example()
{
$spy = $this->createMock(\App\Services\Logger::class);
// Spy يسجل الاستدعاءات
$spy->expects($this->once())
->method('log')
->with('تم تسجيل المستخدم', $this->anything());
$service = new \App\Services\UserService($spy);
$service->registerUser(['name' => 'فاطمة', 'email' => 'fatima@example.com']);
// Spy يتحقق من أن الاستدعاء تم
}
// MOCK - توقعات مبرمجة مسبقاً (التحقق من السلوك الصارم)
public function test_mock_example()
{
$mock = $this->createMock(\App\Services\PaymentGateway::class);
// Mock يتوقع استدعاءات محددة بترتيب محدد
$mock->expects($this->once())
->method('charge')
->with(100.00, 'USD')
->willReturn(['success' => true, 'transaction_id' => 'txn_123']);
$service = new \App\Services\OrderService($mock);
$result = $service->processPayment(100.00, 'USD');
$this->assertTrue($result['success']);
}
// FAKE - تطبيق عملي (تزييفات Laravel)
public function test_fake_example()
{
\Illuminate\Support\Facades\Mail::fake();
$service = new \App\Services\NotificationService();
$service->sendWelcomeEmail('user@example.com');
// Fake التقط البريد
\Illuminate\Support\Facades\Mail::assertSent(\App\Mail\WelcomeEmail::class);
}
}
محاكاة التبعيات باستخدام Mockery
يستخدم Laravel Mockery لإنشاء كائنات محاكاة. يوفر Mockery بناء جملة تعبيري لتحديد التوقعات والتحقق من التفاعلات.
<?php
namespace Tests\Unit;
use App\Services\PaymentGateway;
use App\Services\OrderService;
use Mockery;
use Tests\TestCase;
class OrderServiceTest extends TestCase
{
// محاكاة أساسية مع قيمة مرجعة
public function test_creates_order_with_successful_payment()
{
$paymentGateway = Mockery::mock(PaymentGateway::class);
$paymentGateway->shouldReceive('charge')
->once()
->with(100.00, 'USD', Mockery::any())
->andReturn([
'success' => true,
'transaction_id' => 'txn_123'
]);
$service = new OrderService($paymentGateway);
$result = $service->createOrder([
'amount' => 100.00,
'currency' => 'USD',
'items' => [...]
]);
$this->assertTrue($result['success']);
$this->assertEquals('txn_123', $result['transaction_id']);
}
// محاكاة مع استدعاءات طرق متعددة
public function test_handles_payment_failure()
{
$paymentGateway = Mockery::mock(PaymentGateway::class);
$paymentGateway->shouldReceive('charge')
->once()
->andReturn(['success' => false, 'error' => 'أموال غير كافية']);
$paymentGateway->shouldReceive('logFailure')
->once()
->with(Mockery::type('string'));
$service = new OrderService($paymentGateway);
$result = $service->createOrder(['amount' => 100.00]);
$this->assertFalse($result['success']);
}
// مطابقة المعاملات
public function test_argument_matching()
{
$gateway = Mockery::mock(PaymentGateway::class);
// مطابقة القيمة الدقيقة
$gateway->shouldReceive('charge')->with(100.00)->once();
// مطابقة النوع
$gateway->shouldReceive('log')->with(Mockery::type('string'))->once();
// مطابقة النمط
$gateway->shouldReceive('notify')
->with(Mockery::pattern('/^user_\d+$/'))
->once();
// مطابقة callback
$gateway->shouldReceive('validate')
->with(Mockery::on(function ($arg) {
return $arg['amount'] > 0 && $arg['currency'] === 'USD';
}))
->once();
// مطابقة مجموعة فرعية من المصفوفة
$gateway->shouldReceive('process')
->with(Mockery::subset(['amount' => 100, 'currency' => 'USD']))
->once();
}
// قيم مرجعة متعددة
public function test_multiple_return_values()
{
$api = Mockery::mock(\App\Services\ExternalApi::class);
// إرجاع قيم مختلفة في الاستدعاءات المتتالية
$api->shouldReceive('fetchData')
->andReturn(
['status' => 'قيد الانتظار'],
['status' => 'قيد المعالجة'],
['status' => 'مكتمل']
);
$this->assertEquals('قيد الانتظار', $api->fetchData()['status']);
$this->assertEquals('قيد المعالجة', $api->fetchData()['status']);
$this->assertEquals('مكتمل', $api->fetchData()['status']);
}
// رمي الاستثناءات
public function test_handles_api_exception()
{
$api = Mockery::mock(\App\Services\ExternalApi::class);
$api->shouldReceive('fetchData')
->andThrow(new \Exception('API غير متاح'));
$service = new \App\Services\DataService($api);
$this->expectException(\Exception::class);
$service->getData();
}
// توقعات مرتبة
public function test_ordered_method_calls()
{
$logger = Mockery::mock(\App\Services\Logger::class);
$logger->shouldReceive('info')->with('بدء العملية')->once()->ordered();
$logger->shouldReceive('debug')->with(Mockery::any())->once()->ordered();
$logger->shouldReceive('info')->with('اكتملت العملية')->once()->ordered();
$service = new \App\Services\ProcessService($logger);
$service->runProcess();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}
التزييفات المدمجة في Laravel
يوفر Laravel تطبيقات مزيفة للخدمات الشائعة، مما يسمح لك باختبار الكود الذي يتفاعل مع هذه الخدمات دون تنفيذ العمليات فعلياً.
<?php
namespace Tests\Feature;
use App\Jobs\ProcessOrder;
use App\Mail\OrderConfirmation;
use App\Models\User;
use App\Notifications\OrderShipped;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FakesExampleTest extends TestCase
{
// تزييف Mail
public function test_sends_order_confirmation_email()
{
Mail::fake();
$user = User::factory()->create();
$order = $this->createOrder($user);
// التأكد من إرسال البريد
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// التأكد من العدد
Mail::assertSent(OrderConfirmation::class, 1);
// التأكد من عدم إرسال البريد
Mail::assertNotSent(\App\Mail\InvoiceEmail::class);
// التأكد من عدم إرسال أي شيء
Mail::assertNothingSent();
}
// تزييف Queue
public function test_dispatches_order_processing_job()
{
Queue::fake();
$order = $this->createOrder();
Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
Queue::assertPushed(ProcessOrder::class, 1);
Queue::assertPushedOn('high-priority', ProcessOrder::class);
Queue::assertNotPushed(\App\Jobs\SendNewsletter::class);
}
// تزييف Storage
public function test_uploads_product_image()
{
Storage::fake('public');
$file = UploadedFile::fake()->image('product.jpg', 600, 400);
$response = $this->post('/products', [
'name' => 'منتج تجريبي',
'image' => $file,
]);
// التأكد من تخزين الملف
Storage::disk('public')->assertExists('products/' . $file->hashName());
// التأكد من محتويات الملف
$this->assertEquals(
file_get_contents($file->path()),
Storage::disk('public')->get('products/' . $file->hashName())
);
// التأكد من عدم وجود الملف
Storage::disk('public')->assertMissing('products/nonexistent.jpg');
}
// تزييف Event
public function test_dispatches_order_created_event()
{
Event::fake([\App\Events\OrderCreated::class]);
$order = $this->createOrder();
Event::assertDispatched(\App\Events\OrderCreated::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
Event::assertDispatched(\App\Events\OrderCreated::class, 1);
Event::assertNotDispatched(\App\Events\OrderCancelled::class);
}
// تزييف Notification
public function test_sends_order_shipped_notification()
{
Notification::fake();
$user = User::factory()->create();
$order = $this->createOrder($user);
$order->markAsShipped();
Notification::assertSentTo(
$user,
OrderShipped::class,
function ($notification) use ($order) {
return $notification->order->id === $order->id;
}
);
Notification::assertSentTo($user, OrderShipped::class, 1);
Notification::assertNotSentTo($user, \App\Notifications\PaymentFailed::class);
}
// تزييف HTTP
public function test_fetches_external_api_data()
{
Http::fake([
'api.example.com/users/*' => Http::response([
'id' => 1,
'name' => 'أحمد محمد'
], 200),
'api.example.com/posts/*' => Http::response([
'error' => 'غير موجود'
], 404),
'*' => Http::response(['default' => 'استجابة'], 200),
]);
$service = app(\App\Services\ExternalApiService::class);
$user = $service->fetchUser(1);
$this->assertEquals('أحمد محمد', $user['name']);
// التأكد من إجراء الطلبات
Http::assertSent(function ($request) {
return $request->url() === 'https://api.example.com/users/1' &&
$request->method() === 'GET';
});
Http::assertSentCount(1);
}
// تزييف Bus (المهام والأوامر)
public function test_dispatches_command_chain()
{
Bus::fake();
$this->artisan('process:orders');
Bus::assertDispatched(ProcessOrder::class);
Bus::assertChained([
\App\Jobs\SendOrderConfirmation::class,
\App\Jobs\UpdateInventory::class,
\App\Jobs\NotifyWarehouse::class,
]);
}
// تزييف Cache
public function test_caches_expensive_calculation()
{
\Illuminate\Support\Facades\Cache::fake();
$service = app(\App\Services\CalculationService::class);
$result = $service->calculateWithCache('key', fn() => 42);
\Illuminate\Support\Facades\Cache::assertHas('calculation:key');
$this->assertEquals(42, $result);
}
}
المحاكيات الجزئية
تسمح لك المحاكيات الجزئية بمحاكاة طرق محددة فقط من فئة مع الاحتفاظ بالتطبيق الحقيقي للآخرين. هذا مفيد عند اختبار الفئات ذات التبعيات المعقدة.
<?php
namespace Tests\Unit;
use App\Services\OrderService;
use Tests\TestCase;
class PartialMockTest extends TestCase
{
// محاكاة جزئية - محاكاة طرق محددة فقط
public function test_partial_mock_with_mockery()
{
$service = Mockery::mock(OrderService::class)->makePartial();
// محاكاة استدعاء API الخارجي فقط
$service->shouldReceive('fetchShippingRates')
->once()
->andReturn([
'standard' => 5.00,
'express' => 15.00,
]);
// جميع الطرق الأخرى تستخدم التطبيق الحقيقي
$order = $service->createOrder([
'items' => [['id' => 1, 'quantity' => 2]],
'shipping_method' => 'standard',
]);
$this->assertEquals(5.00, $order->shipping_cost);
}
// محاكاة جزئية مع spy في Laravel
public function test_partial_mock_with_spy()
{
$service = $this->partialMock(OrderService::class, function ($mock) {
$mock->shouldReceive('sendConfirmationEmail')
->once()
->andReturn(true);
});
// يتم استدعاء التطبيق الحقيقي
$order = $service->createOrder(['items' => [...]]);
// التحقق من استدعاء الطريقة المحاكاة
$this->assertTrue($order->exists);
}
// محاكاة الطرق المحمية/الخاصة (للاختبار)
public function test_mock_protected_method()
{
$service = Mockery::mock(OrderService::class)->makePartial();
// استخدام shouldAllowMockingProtectedMethods() للطرق المحمية
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('calculateTax')
->once()
->andReturn(10.00);
$total = $service->calculateTotal(['subtotal' => 100.00]);
$this->assertEquals(110.00, $total);
}
// Spy - مثل المحاكاة الجزئية لكن يتحقق بعد التنفيذ
public function test_spy_example()
{
$service = $this->spy(OrderService::class);
// تنفيذ التطبيق الحقيقي
$order = $service->createOrder([
'user_id' => 1,
'items' => [['id' => 1, 'quantity' => 2]],
]);
// التحقق من استدعاء الطرق
$service->shouldHaveReceived('validateItems')->once();
$service->shouldHaveReceived('calculateTotal')->once();
$service->shouldHaveReceived('saveOrder')->once();
}
}
أنماط المحاكاة المتقدمة
سيناريوهات الاختبار المعقدة غالباً ما تتطلب تقنيات محاكاة متطورة بما في ذلك تسلسل الطرق والواجهات السلسة وربط الحاوية.
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Mockery;
class AdvancedMockingTest extends TestCase
{
// محاكاة تسلسل الطرق / الواجهات السلسة
public function test_mock_fluent_interface()
{
$query = Mockery::mock(\Illuminate\Database\Query\Builder::class);
$query->shouldReceive('where')
->with('status', 'active')
->once()
->andReturnSelf();
$query->shouldReceive('orderBy')
->with('created_at', 'desc')
->once()
->andReturnSelf();
$query->shouldReceive('limit')
->with(10)
->once()
->andReturnSelf();
$query->shouldReceive('get')
->once()
->andReturn(collect([
['id' => 1, 'name' => 'اختبار']
]));
$result = $query->where('status', 'active')
->orderBy('created_at', 'desc')
->limit(10)
->get();
$this->assertCount(1, $result);
}
// ربط المحاكيات بالحاوية
public function test_bind_mock_to_container()
{
$mock = Mockery::mock(\App\Services\PaymentGateway::class);
$mock->shouldReceive('charge')
->once()
->andReturn(['success' => true]);
// ربط المحاكاة بالحاوية
$this->app->instance(\App\Services\PaymentGateway::class, $mock);
// Controller أو الخدمة ستستقبل المحاكاة
$response = $this->post('/orders', [
'amount' => 100,
'items' => [...]
]);
$response->assertStatus(201);
}
// محاكاة مع معاملات البناء
public function test_mock_with_constructor()
{
$config = ['api_key' => 'test_key', 'timeout' => 30];
$service = Mockery::mock(
\App\Services\ExternalApiClient::class,
[$config]
)->makePartial();
$service->shouldReceive('makeRequest')
->once()
->andReturn(['data' => 'استجابة']);
$result = $service->fetchData();
$this->assertEquals('استجابة', $result['data']);
}
// محاكاة الطرق الثابتة
public function test_mock_static_method()
{
// إنشاء محاكاة اسم مستعار للطرق الثابتة
$mock = Mockery::mock('alias:' . \App\Services\HelperService::class);
$mock->shouldReceive('generateToken')
->once()
->andReturn('test_token_123');
// الاستدعاء الثابت سيستخدم المحاكاة
$token = \App\Services\HelperService::generateToken();
$this->assertEquals('test_token_123', $token);
}
// مطابقة معاملات معقدة مع callbacks
public function test_complex_argument_matching()
{
$repo = Mockery::mock(\App\Repositories\UserRepository::class);
$repo->shouldReceive('create')
->once()
->with(Mockery::on(function ($data) {
// منطق التحقق المعقد
return isset($data['email']) &&
filter_var($data['email'], FILTER_VALIDATE_EMAIL) &&
isset($data['password']) &&
strlen($data['password']) >= 8 &&
isset($data['terms_accepted']) &&
$data['terms_accepted'] === true;
}))
->andReturn(new \App\Models\User([
'id' => 1,
'email' => 'test@example.com'
]));
$service = new \App\Services\RegistrationService($repo);
$user = $service->register([
'email' => 'test@example.com',
'password' => 'password123',
'terms_accepted' => true,
]);
$this->assertEquals(1, $user->id);
}
}
تمرين 1: محاكاة بوابة الدفع الخارجية
اختبر خدمة معالجة الطلبات التي تستخدم بوابة دفع خارجية:
- أنشئ واجهة PaymentGateway بطرق: charge()، refund()، checkStatus()
- اكتب اختبارات تحاكي البوابة لدفع ناجح ودفع فاشل وسيناريوهات مهلة
- حاكي البوابة لرمي استثناءات لأخطاء الشبكة
- تحقق من أن خدمة الطلبات تتعامل مع جميع السيناريوهات بشكل صحيح
- استخدم مطابقة المعاملات للتحقق من تمرير المبالغ والعملات الصحيحة
تمرين 2: الاختبار باستخدام تزييفات Laravel
اختبر تدفق تسجيل المستخدم باستخدام تزييفات Laravel:
- استخدم Mail::fake() للتحقق من إرسال بريد الترحيب الإلكتروني
- استخدم Queue::fake() للتحقق من إرسال مهمة التحقق من البريد الإلكتروني
- استخدم Event::fake() للتحقق من إطلاق حدث UserRegistered
- استخدم Storage::fake() لاختبار تحميل صورة الملف الشخصي
- تحقق من جميع التزييفات بتأكيدات مناسبة (assertSent، assertPushed، assertDispatched)
- اختبر أن بريد الترحيب الإلكتروني يحتوي على اسم المستخدم
تمرين 3: المحاكيات الجزئية والجواسيس
اختبر خدمة توليد التقارير باستخدام المحاكيات الجزئية:
- أنشئ ReportService بطرق: generateReport()، fetchData()، formatData()، saveToFile()
- استخدم محاكاة جزئية لمحاكاة طريقة fetchData() فقط (محاكاة API خارجي بطيء)
- دع formatData() و saveToFile() تستخدم التطبيق الحقيقي
- استخدم spy للتحقق من استدعاء formatData() بهيكل بيانات صحيح
- تحقق من استدعاء saveToFile() ببيانات منسقة بشكل صحيح
- اختبر أن سير عمل توليد التقرير الكامل يعمل بشكل صحيح
الخلاصة
في هذا الدرس، أتقنت تقنيات الاختبار المتقدمة لعزل الكود عن التبعيات الخارجية. تعلمت الأنواع المختلفة من بدائل الاختبار (dummies، stubs، spies، mocks، fakes) ومتى تستخدم كل منها، واستكشفت Mockery لإنشاء محاكيات متطورة مع التوقعات ومطابقة المعاملات، واستخدمت تزييفات Laravel المدمجة لـ Mail و Queue و Storage و Events و Notifications و HTTP، ونفذت محاكيات جزئية وجواسيس لاختبار طرق محددة مع الحفاظ على التطبيقات الحقيقية، وطبقت أنماط محاكاة متقدمة للواجهات السلسة وربط الحاوية والسيناريوهات المعقدة. تمكّنك هذه التقنيات من كتابة اختبارات سريعة وموثوقة تتحقق من منطق التطبيق الخاص بك بشكل مستقل عن الأنظمة الخارجية.