WebSockets والتطبيقات الفورية
اختبار التطبيقات في الوقت الفعلي
اختبار التطبيقات في الوقت الفعلي
يتطلب اختبار التطبيقات في الوقت الفعلي أساليب خاصة بسبب طبيعتها غير المتزامنة والاتصالات ذات الحالة. في هذا الدرس، سنستكشف استراتيجيات اختبار شاملة لتطبيقات WebSocket و Socket.io.
اختبار اتصالات WebSocket
يتضمن الاختبار الأساسي لاتصال WebSocket التحقق من إنشاء الاتصالات ونقل الرسائل وإغلاق الاتصالات بشكل صحيح.
// اختبار WebSocket الأساسي مع Jest
const WebSocket = require('ws');
describe('WebSocket Connection', () => {
let wss, ws;
beforeAll((done) => {
// بدء خادم WebSocket
wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (socket) => {
socket.on('message', (message) => {
socket.send(`Echo: ${message}`);
});
});
done();
});
afterAll(() => {
wss.close();
});
test('should connect to WebSocket server', (done) => {
ws = new WebSocket('ws://localhost:8080');
ws.on('open', () => {
expect(ws.readyState).toBe(WebSocket.OPEN);
done();
});
});
test('should send and receive messages', (done) => {
const testMessage = 'Hello WebSocket';
ws.on('message', (data) => {
expect(data.toString()).toBe(`Echo: ${testMessage}`);
ws.close();
done();
});
ws.send(testMessage);
});
});
محاكاة Socket.io
يتطلب Socket.io استراتيجيات محاكاة محددة لاختبار تفاعلات العميل والخادم بشكل مستقل.
// محاكاة عميل Socket.io
const io = require('socket.io-client');
const ioServer = require('socket.io');
const http = require('http');
describe('Socket.io Server', () => {
let httpServer, ioServerInstance, clientSocket;
beforeAll((done) => {
httpServer = http.createServer();
ioServerInstance = ioServer(httpServer);
httpServer.listen(() => {
const port = httpServer.address().port;
clientSocket = io(`http://localhost:${port}`);
ioServerInstance.on('connection', (socket) => {
socket.on('ping', (callback) => {
callback({ message: 'pong' });
});
});
clientSocket.on('connect', done);
});
});
afterAll(() => {
ioServerInstance.close();
clientSocket.close();
httpServer.close();
});
test('should respond to ping with pong', (done) => {
clientSocket.emit('ping', (response) => {
expect(response.message).toBe('pong');
done();
});
});
});
اختبار الأحداث والمعالجات
اختبر معالجات الأحداث للتأكد من معالجة البيانات بشكل صحيح وإصدار استجابات مناسبة.
// اختبار الأحداث المخصصة
describe('Chat Events', () => {
let server, client;
beforeEach((done) => {
server = createTestServer();
client = createTestClient(server);
client.on('connect', done);
});
test('should broadcast chat messages', (done) => {
const secondClient = createTestClient(server);
secondClient.on('chat:message', (data) => {
expect(data.username).toBe('TestUser');
expect(data.message).toBe('Hello everyone');
done();
});
client.emit('chat:send', {
username: 'TestUser',
message: 'Hello everyone'
});
});
test('should reject empty messages', (done) => {
client.emit('chat:send', { message: '' }, (error) => {
expect(error).toBeDefined();
expect(error.code).toBe('EMPTY_MESSAGE');
done();
});
});
test('should emit typing indicators', (done) => {
const listener = createTestClient(server);
listener.on('user:typing', (data) => {
expect(data.username).toBe('TestUser');
expect(data.isTyping).toBe(true);
done();
});
client.emit('typing:start', { username: 'TestUser' });
});
});
أفضل ممارسة: استخدم قواعد بيانات اختبار منفصلة وبيئات اختبار معزولة لمنع التداخل بين الاختبارات. نظف الاتصالات والموارد بعد كل اختبار.
اختبار التكامل مع socket.io-client
تتحقق اختبارات التكامل من تدفق الاتصال الكامل بين العميل والخادم.
// اختبار تكامل كامل
const request = require('supertest');
const app = require('../app');
describe('Real-Time Integration', () => {
let server, client1, client2;
beforeAll((done) => {
server = app.listen(3000, () => {
client1 = io('http://localhost:3000', {
auth: { token: 'user1-token' }
});
client2 = io('http://localhost:3000', {
auth: { token: 'user2-token' }
});
let connected = 0;
const checkDone = () => {
connected++;
if (connected === 2) done();
};
client1.on('connect', checkDone);
client2.on('connect', checkDone);
});
});
afterAll(() => {
client1.close();
client2.close();
server.close();
});
test('should handle room joining and messaging', (done) => {
const roomId = 'test-room';
const testMessage = 'Integration test message';
client2.on('room:message', (data) => {
expect(data.room).toBe(roomId);
expect(data.message).toBe(testMessage);
done();
});
client1.emit('room:join', { roomId }, () => {
client2.emit('room:join', { roomId }, () => {
client1.emit('room:send', {
roomId,
message: testMessage
});
});
});
});
test('should handle user disconnection', (done) => {
client2.on('user:left', (data) => {
expect(data.userId).toBeDefined();
done();
});
client1.disconnect();
});
});
اختبار منطق إعادة الاتصال
سلوك إعادة الاتصال حاسم للموثوقية. اختبر إعادة الاتصال التلقائي واستعادة الحالة.
// اختبار إعادة الاتصال
describe('Reconnection Behavior', () => {
let server, client;
beforeEach((done) => {
server = createTestServer();
client = io('http://localhost:3000', {
reconnection: true,
reconnectionDelay: 100,
reconnectionAttempts: 3
});
client.on('connect', done);
});
test('should reconnect after disconnection', (done) => {
let reconnected = false;
client.on('reconnect', () => {
reconnected = true;
expect(client.connected).toBe(true);
done();
});
// محاكاة إعادة تشغيل الخادم
server.close();
setTimeout(() => {
server = createTestServer();
}, 200);
});
test('should restore state after reconnection', (done) => {
const roomId = 'persistent-room';
client.emit('room:join', { roomId }, () => {
client.on('reconnect', () => {
client.emit('room:check', (response) => {
expect(response.rooms).toContain(roomId);
done();
});
});
// إجبار إعادة الاتصال
client.io.engine.close();
});
});
test('should emit reconnection attempts', (done) => {
let attemptCount = 0;
client.on('reconnect_attempt', (attempt) => {
attemptCount++;
expect(attempt).toBeGreaterThan(0);
});
client.on('reconnect_failed', () => {
expect(attemptCount).toBe(3);
done();
});
// إغلاق الخادم نهائيًا
server.close();
client.io.engine.close();
});
});
اختبار الحمل مع Artillery
يضمن اختبار الحمل أن تطبيق الوقت الفعلي الخاص بك يمكنه التعامل مع اتصالات متزامنة متعددة وإنتاجية رسائل عالية.
# تكوين Artillery (artillery.yml)
config:
target: 'http://localhost:3000'
socketio:
transports: ['websocket']
phases:
- duration: 60
arrivalRate: 10
name: 'Warm up'
- duration: 120
arrivalRate: 50
name: 'Sustained load'
- duration: 60
arrivalRate: 100
name: 'Peak load'
processor: './load-test-processor.js'
scenarios:
- name: 'Chat Simulation'
engine: socketio
flow:
- emit:
channel: 'user:join'
data:
username: '{{ $randomString() }}'
- think: 2
- emit:
channel: 'room:join'
data:
roomId: 'lobby'
- think: 3
- loop:
- emit:
channel: 'chat:send'
data:
message: '{{ $randomString() }}'
- think: 5
count: 10
// معالج Artillery مخصص
module.exports = {
generateRandomMessage: function(context, events, done) {
const messages = [
'Hello everyone!',
'How are you?',
'This is a test message',
'WebSocket performance testing'
];
context.vars.randomMessage =
messages[Math.floor(Math.random() * messages.length)];
return done();
},
validateResponse: function(context, events, done) {
events.on('response', (data) => {
if (!data || !data.message) {
events.emit('error', 'Invalid response format');
}
});
return done();
}
};
مقاييس اختبار الحمل: راقب عدد الاتصالات، وزمن انتقال الرسائل، واستخدام الذاكرة، واستخدام المعالج، ومعدلات الأخطاء. استخدم أدوات مثل Artillery أو k6 أو نصوص Node.js المخصصة لاختبار حمل WebSocket.
// نص اختبار حمل يدوي
const io = require('socket.io-client');
async function loadTest(concurrentUsers, duration) {
const clients = [];
const stats = {
messagesReceived: 0,
messagesSent: 0,
errors: 0,
latencies: []
};
// إنشاء الاتصالات
for (let i = 0; i < concurrentUsers; i++) {
const client = io('http://localhost:3000');
client.on('message', () => {
stats.messagesReceived++;
});
client.on('error', () => {
stats.errors++;
});
clients.push(client);
}
// إرسال الرسائل بشكل دوري
const interval = setInterval(() => {
clients.forEach(client => {
const startTime = Date.now();
client.emit('ping', () => {
stats.latencies.push(Date.now() - startTime);
stats.messagesSent++;
});
});
}, 1000);
// التوقف بعد المدة
setTimeout(() => {
clearInterval(interval);
clients.forEach(c => c.close());
const avgLatency = stats.latencies.reduce((a, b) => a + b, 0)
/ stats.latencies.length;
console.log('نتائج اختبار الحمل:');
console.log(` المستخدمون المتزامنون: ${concurrentUsers}`);
console.log(` الرسائل المرسلة: ${stats.messagesSent}`);
console.log(` الرسائل المستلمة: ${stats.messagesReceived}`);
console.log(` الأخطاء: ${stats.errors}`);
console.log(` متوسط الكمون: ${avgLatency.toFixed(2)}ms`);
}, duration);
}
loadTest(100, 30000); // 100 مستخدم لمدة 30 ثانية
تمرين تطبيقي:
- اكتب مجموعة اختبارات لنظام إشعارات في الوقت الفعلي يتحقق من توصيل الرسائل للمستخدمين المشتركين
- أنشئ اختبارات تكامل لردهة لعبة متعددة اللاعبين (انضمام، مغادرة، استعداد)
- نفذ اختبار حمل يحاكي 1000 مستخدم دردشة متزامن
- اكتب اختبارات لسلوك إعادة الاتصال مع استمرارية قائمة انتظار الرسائل
- أنشئ اختبارًا يتحقق من التنظيف الصحيح لموارد المستخدم المنقطع