اختبار التطبيقات في الوقت الفعلي
يتطلب اختبار التطبيقات في الوقت الفعلي أساليب خاصة بسبب طبيعتها غير المتزامنة والاتصالات ذات الحالة. في هذا الدرس، سنستكشف استراتيجيات اختبار شاملة لتطبيقات 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 مستخدم دردشة متزامن
- اكتب اختبارات لسلوك إعادة الاتصال مع استمرارية قائمة انتظار الرسائل
- أنشئ اختبارًا يتحقق من التنظيف الصحيح لموارد المستخدم المنقطع