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

اختبار WebSockets والميزات في الوقت الفعلي

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

اختبار WebSockets والميزات في الوقت الفعلي

الميزات في الوقت الفعلي التي تعمل بواسطة WebSockets وبث الأحداث ضرورية لتطبيقات الويب الحديثة. يتطلب اختبار هذه الميزات تقنيات خاصة لمحاكاة الاتصالات والأحداث والتفاعلات في الوقت الفعلي.

فهم بث Laravel

يوفر Laravel واجهة برمجة تطبيقات موحدة لبث الأحداث عبر برامج تشغيل مختلفة (Pusher، Ably، Redis، إلخ). سنركز على اختبار هذه الميزات بفعالية.

أساسيات البث

<?php // فئة الحدث التي تبث namespace App\Events; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Queue\SerializesModels; class OrderStatusUpdated implements ShouldBroadcast { use InteractsWithSockets, SerializesModels; public function __construct( public Order $order ) {} public function broadcastOn(): Channel { return new Channel('orders.' . $this->order->id); } public function broadcastAs(): string { return 'status.updated'; } public function broadcastWith(): array { return [ 'id' => $this->order->id, 'status' => $this->order->status, 'updated_at' => $this->order->updated_at->toISOString(), ]; } }

اختبار أحداث البث

اختبار بث الحدث الأساسي

<?php namespace Tests\Feature; use Tests\TestCase; use App\Models\Order; use App\Events\OrderStatusUpdated; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Testing\RefreshDatabase; class OrderBroadcastingTest extends TestCase { use RefreshDatabase; /** @test */ public function order_status_update_broadcasts_event() { Event::fake(); $order = Order::factory()->create(['status' => 'pending']); // تشغيل الإجراء الذي يبث $order->update(['status' => 'processing']); // التأكيد على أن الحدث تم بثه Event::assertDispatched(OrderStatusUpdated::class, function ($event) use ($order) { return $event->order->id === $order->id && $event->order->status === 'processing'; }); } /** @test */ public function event_broadcasts_on_correct_channel() { $order = Order::factory()->create(); $event = new OrderStatusUpdated($order); $channel = $event->broadcastOn(); $this->assertEquals('orders.' . $order->id, $channel->name); } /** @test */ public function event_broadcasts_with_correct_data() { $order = Order::factory()->create([ 'id' => 123, 'status' => 'completed' ]); $event = new OrderStatusUpdated($order); $data = $event->broadcastWith(); $this->assertEquals([ 'id' => 123, 'status' => 'completed', 'updated_at' => $order->updated_at->toISOString(), ], $data); } /** @test */ public function event_broadcasts_as_correct_name() { $order = Order::factory()->create(); $event = new OrderStatusUpdated($order); $this->assertEquals('status.updated', $event->broadcastAs()); } }

اختبار القنوات الخاصة

تتطلب القنوات الخاصة المصادقة. إليك كيفية اختبارها:

<?php // حدث القناة الخاصة class UserNotification implements ShouldBroadcast { public function __construct( public User $user, public string $message ) {} public function broadcastOn(): Channel { return new PrivateChannel('users.' . $this->user->id); } } // اختبار القنوات الخاصة /** @test */ public function private_channel_requires_authentication() { $user = User::factory()->create(); // الضيف لا يمكنه الوصول إلى القناة الخاصة $this->get('/broadcasting/auth', [ 'channel_name' => 'private-users.' . $user->id ])->assertUnauthorized(); // المستخدم المصادق عليه يمكنه الوصول إلى قناته $this->actingAs($user) ->get('/broadcasting/auth', [ 'channel_name' => 'private-users.' . $user->id ])->assertOk(); } /** @test */ public function user_cannot_access_another_users_private_channel() { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $this->actingAs($user1) ->get('/broadcasting/auth', [ 'channel_name' => 'private-users.' . $user2->id ])->assertForbidden(); }

اختبار قنوات الحضور

تتبع قنوات الحضور من المشترك في القناة:

<?php // routes/channels.php Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) { if ($user->canAccessRoom($roomId)) { return [ 'id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url, ]; } }); // اختبار قنوات الحضور /** @test */ public function user_can_join_authorized_presence_channel() { $user = User::factory()->create(); $room = ChatRoom::factory()->create(); $room->members()->attach($user); $response = $this->actingAs($user) ->post('/broadcasting/auth', [ 'channel_name' => 'presence-chat.' . $room->id, 'socket_id' => '123.456' ]); $response->assertOk() ->assertJson([ 'channel_data' => [ 'user_id' => $user->id, 'user_info' => [ 'id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url, ] ] ]); } /** @test */ public function user_cannot_join_unauthorized_presence_channel() { $user = User::factory()->create(); $room = ChatRoom::factory()->create(); // المستخدم ليس عضوًا $response = $this->actingAs($user) ->post('/broadcasting/auth', [ 'channel_name' => 'presence-chat.' . $room->id, 'socket_id' => '123.456' ]); $response->assertForbidden(); }

اختبار رسائل WebSocket

استخدام تقليد البث

<?php use Illuminate\Support\Facades\Broadcast; /** @test */ public function message_is_broadcast_when_sent() { Broadcast::fake(); $user = User::factory()->create(); $room = ChatRoom::factory()->create(); $this->actingAs($user) ->post('/api/chat/' . $room->id . '/messages', [ 'message' => 'Hello, World!' ])->assertCreated(); Broadcast::assertBroadcastOn( 'presence-chat.' . $room->id, MessageSent::class ); } /** @test */ public function broadcast_contains_correct_message_data() { Event::fake(); $user = User::factory()->create(); $room = ChatRoom::factory()->create(); $this->actingAs($user) ->post('/api/chat/' . $room->id . '/messages', [ 'message' => 'Test message' ]); Event::assertDispatched(MessageSent::class, function ($event) { return $event->message->body === 'Test message'; }); }

اختبار الإشعارات في الوقت الفعلي

<?php // إشعار يبث namespace App\Notifications; use Illuminate\Notifications\Notification; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; class OrderShipped extends Notification implements ShouldBroadcast { public function __construct( public Order $order ) {} public function via($notifiable): array { return ['database', 'broadcast']; } public function toBroadcast($notifiable): BroadcastMessage { return new BroadcastMessage([ 'order_id' => $this->order->id, 'message' => 'Your order has been shipped!', 'tracking_number' => $this->order->tracking_number, ]); } public function broadcastType(): string { return 'order.shipped'; } } // اختبار بث الإشعار use Illuminate\Support\Facades\Notification; /** @test */ public function notification_is_broadcast_to_user() { Notification::fake(); $user = User::factory()->create(); $order = Order::factory()->create(['user_id' => $user->id]); // تشغيل الإشعار $order->ship('TRACK123'); Notification::assertSentTo($user, OrderShipped::class); } /** @test */ public function broadcast_notification_contains_correct_data() { $user = User::factory()->create(); $order = Order::factory()->create([ 'id' => 123, 'tracking_number' => 'TRACK123' ]); $notification = new OrderShipped($order); $data = $notification->toBroadcast($user); $this->assertEquals([ 'order_id' => 123, 'message' => 'Your order has been shipped!', 'tracking_number' => 'TRACK123', ], $data->data); }

اختبار التكامل مع Laravel Echo

اختبار تكامل عميل Echo

<?php // اختبار JavaScript مع Jest describe('Real-time Order Updates', () => { let echo; beforeEach(() => { echo = new Echo({ broadcaster: 'pusher', key: process.env.PUSHER_APP_KEY, cluster: process.env.PUSHER_APP_CLUSTER, forceTLS: true }); }); afterEach(() => { echo.disconnect(); }); test('receives order status updates', (done) => { const orderId = 123; echo.channel(`orders.${orderId}`) .listen('status.updated', (event) => { expect(event.id).toBe(orderId); expect(event.status).toBe('processing'); done(); }); // تشغيل الحدث من الخادم axios.patch(`/api/orders/${orderId}`, { status: 'processing' }); }); test('handles connection errors gracefully', (done) => { echo.connector.pusher.connection.bind('error', (error) => { expect(error).toBeDefined(); done(); }); // تشغيل خطأ الاتصال echo.connector.pusher.connection.emit('error', { error: 'Connection failed' }); }); });

الاختبار مع تقليد Pusher

<?php use Pusher\Pusher; use Mockery; /** @test */ public function event_is_pushed_to_pusher() { // تقليد Pusher $pusherMock = Mockery::mock(Pusher::class); $pusherMock->shouldReceive('trigger') ->once() ->with( 'orders.123', 'status.updated', Mockery::on(function ($data) { return $data['status'] === 'processing'; }) ) ->andReturn(true); $this->app->instance(Pusher::class, $pusherMock); $order = Order::factory()->create(['id' => 123]); $order->update(['status' => 'processing']); }

اختبار بث Redis

<?php use Illuminate\Support\Facades\Redis; /** @test */ public function event_is_published_to_redis() { Redis::shouldReceive('connection->publish') ->once() ->with( Mockery::type('string'), Mockery::on(function ($payload) { $data = json_decode($payload, true); return $data['event'] === 'App\\Events\\OrderStatusUpdated'; }) ); $order = Order::factory()->create(); broadcast(new OrderStatusUpdated($order)); }

اختبار شامل للوقت الفعلي

<?php use Laravel\Dusk\Browser; /** @test */ public function user_receives_real_time_notification() { $this->browse(function (Browser $browser) { $user = User::factory()->create(); $browser->loginAs($user) ->visit('/dashboard') ->waitFor('#notifications'); // تشغيل الإشعار من الخادم $user->notify(new OrderShipped(Order::factory()->create())); // انتظار ظهور الإشعار في الوقت الفعلي $browser->waitForText('Your order has been shipped!', 5) ->assertSee('TRACK123'); }); } /** @test */ public function chat_messages_appear_in_real_time() { $this->browse(function (Browser $sender, Browser $receiver) { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $room = ChatRoom::factory()->create(); $sender->loginAs($user1)->visit('/chat/' . $room->id); $receiver->loginAs($user2)->visit('/chat/' . $room->id); $sender->type('message', 'Hello from user 1!') ->press('Send'); // يجب أن يرى المستقبل الرسالة في الوقت الفعلي $receiver->waitForText('Hello from user 1!', 5) ->assertSee($user1->name); }); }

اختبار دورة حياة اتصال WebSocket

<?php /** @test */ public function tracks_user_presence_in_chat_room() { Event::fake(); $user = User::factory()->create(); $room = ChatRoom::factory()->create(); // محاكاة انضمام المستخدم $this->actingAs($user) ->post('/api/chat/' . $room->id . '/join') ->assertOk(); Event::assertDispatched(UserJoinedRoom::class, function ($event) use ($user, $room) { return $event->user->id === $user->id && $event->room->id === $room->id; }); // محاكاة مغادرة المستخدم $this->actingAs($user) ->post('/api/chat/' . $room->id . '/leave') ->assertOk(); Event::assertDispatched(UserLeftRoom::class, function ($event) use ($user, $room) { return $event->user->id === $user->id && $event->room->id === $room->id; }); }

اختبار الأداء للميزات في الوقت الفعلي

<?php /** @test */ public function handles_multiple_concurrent_broadcasts() { Event::fake(); $users = User::factory()->count(100)->create(); $startTime = microtime(true); foreach ($users as $user) { broadcast(new UserNotification($user, 'Test message')); } $endTime = microtime(true); $duration = $endTime - $startTime; Event::assertDispatchedTimes(UserNotification::class, 100); // التأكد من اكتمال البث في وقت مقبول $this->assertLessThan(2.0, $duration, 'Broadcasting took too long'); } /** @test */ public function queued_broadcasts_are_processed_correctly() { Queue::fake(); $order = Order::factory()->create(); broadcast(new OrderStatusUpdated($order))->toOthers(); Queue::assertPushed(BroadcastEvent::class, function ($job) { return $job->event instanceof OrderStatusUpdated; }); }
أفضل الممارسات لاختبار الميزات في الوقت الفعلي:
  • استخدم Event::fake(): لاختبارات الوحدة، قم بتقليد الأحداث لتجنب البث الفعلي
  • اختبر ترخيص القناة: تأكد من تأمين القنوات الخاصة/الحضور بشكل صحيح
  • تحقق من هيكل الحمولة: تحقق من أن بيانات البث تتطابق مع توقعات العميل
  • اختبر حالات الاتصال: تعامل مع الحالات المتصلة وغير المتصلة والأخطاء
  • الأداء مهم: اختبر أداء البث تحت الحمل
الأخطاء الشائعة:
  • نسيان تقليد البث في الاختبارات (مما يسبب مكالمات API فعلية)
  • عدم اختبار ترخيص القناة بشكل صحيح
  • تجاهل شروط السباق في التحديثات في الوقت الفعلي
  • عدم التعامل مع منطق إعادة الاتصال
  • فقدان اختبارات المهلة للاتصالات البطيئة
تمرين تطبيقي:

أنشئ مجموعة اختبار شاملة لنظام إشعارات في الوقت الفعلي يقوم بـ:

  1. بث الإشعارات لمستخدمين محددين
  2. دعم وضع علامة على الإشعارات كمقروءة في الوقت الفعلي
  3. إظهار حضور متصل/غير متصل للمستخدمين
  4. التعامل مع حذف الإشعارات
  5. اختبار ترخيص القناة الخاصة

الخلاصة

اختبار الميزات في الوقت الفعلي يتطلب:

  • تقليد الأحداث: استخدم تقليد Event و Broadcast في Laravel لاختبارات الوحدة
  • اختبار القناة: تحقق من سلوك القنوات العامة والخاصة وقنوات الحضور
  • الترخيص: اختبر أن القنوات محمية بشكل صحيح
  • التحقق من البيانات: تأكد من أن حمولات البث تتطابق مع التوقعات
  • اختبارات التكامل: استخدم Dusk أو اختبارات JavaScript لسيناريوهات شاملة كاملة
  • الأداء: اختبر سلوك النظام تحت الحمل المتزامن