384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
/**
|
||
* NetworkClient 集成测试
|
||
* 测试网络客户端的完整功能,包括依赖注入和错误处理
|
||
*/
|
||
|
||
import { NetworkClient } from '../src/core/NetworkClient';
|
||
|
||
// Mock 所有外部依赖
|
||
jest.mock('@esengine/ecs-framework', () => ({
|
||
Core: {
|
||
scene: null,
|
||
schedule: {
|
||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||
stop: jest.fn()
|
||
}))
|
||
}
|
||
},
|
||
Emitter: jest.fn().mockImplementation(() => ({
|
||
emit: jest.fn(),
|
||
on: jest.fn(),
|
||
off: jest.fn(),
|
||
removeAllListeners: jest.fn()
|
||
}))
|
||
}));
|
||
|
||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||
NetworkValue: {},
|
||
generateMessageId: jest.fn(() => 'test-message-id-123'),
|
||
generateNetworkId: jest.fn(() => 12345),
|
||
NetworkUtils: {
|
||
generateMessageId: jest.fn(() => 'test-message-id-456'),
|
||
calculateDistance: jest.fn(() => 100),
|
||
isNodeEnvironment: jest.fn(() => false),
|
||
isBrowserEnvironment: jest.fn(() => true)
|
||
}
|
||
}));
|
||
|
||
// Mock WebSocket
|
||
class MockWebSocket {
|
||
public readyState: number = WebSocket.CONNECTING;
|
||
public onopen: ((event: Event) => void) | null = null;
|
||
public onclose: ((event: CloseEvent) => void) | null = null;
|
||
public onmessage: ((event: MessageEvent) => void) | null = null;
|
||
public onerror: ((event: Event) => void) | null = null;
|
||
|
||
constructor(public url: string, public protocols?: string | string[]) {}
|
||
|
||
send(data: string | ArrayBuffer | Blob): void {}
|
||
close(code?: number, reason?: string): void {
|
||
this.readyState = WebSocket.CLOSED;
|
||
if (this.onclose) {
|
||
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
||
}
|
||
}
|
||
}
|
||
|
||
(global as any).WebSocket = MockWebSocket;
|
||
(global as any).WebSocket.CONNECTING = 0;
|
||
(global as any).WebSocket.OPEN = 1;
|
||
(global as any).WebSocket.CLOSING = 2;
|
||
(global as any).WebSocket.CLOSED = 3;
|
||
|
||
describe('NetworkClient 集成测试', () => {
|
||
let client: NetworkClient;
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
afterEach(() => {
|
||
if (client) {
|
||
client.disconnect().catch(() => {});
|
||
client = null as any;
|
||
}
|
||
});
|
||
|
||
describe('依赖注入测试', () => {
|
||
it('应该正确处理所有依赖模块', () => {
|
||
expect(() => {
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
}).not.toThrow();
|
||
|
||
expect(client).toBeInstanceOf(NetworkClient);
|
||
});
|
||
|
||
it('应该正确使用network-shared中的工具函数', () => {
|
||
const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared');
|
||
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
|
||
// 验证network-shared模块被正确导入
|
||
expect(generateMessageId).toBeDefined();
|
||
expect(NetworkUtils).toBeDefined();
|
||
});
|
||
|
||
it('应该正确使用ecs-framework中的Core模块', () => {
|
||
const { Core } = require('@esengine/ecs-framework');
|
||
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
|
||
expect(Core).toBeDefined();
|
||
expect(Core.schedule).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('构造函数错误处理', () => {
|
||
it('应该处理network-shared模块导入失败', () => {
|
||
// 重置模块并模拟导入失败
|
||
jest.resetModules();
|
||
jest.doMock('@esengine/ecs-framework-network-shared', () => {
|
||
throw new Error('network-shared模块导入失败');
|
||
});
|
||
|
||
expect(() => {
|
||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||
new NetworkClient({
|
||
transportType: 'websocket',
|
||
host: 'localhost',
|
||
port: 8080
|
||
});
|
||
}).toThrow();
|
||
});
|
||
|
||
it('应该处理ecs-framework模块导入失败', () => {
|
||
// 重置模块并模拟导入失败
|
||
jest.resetModules();
|
||
jest.doMock('@esengine/ecs-framework', () => {
|
||
throw new Error('ecs-framework模块导入失败');
|
||
});
|
||
|
||
expect(() => {
|
||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||
new NetworkClient({
|
||
transportType: 'websocket',
|
||
host: 'localhost',
|
||
port: 8080
|
||
});
|
||
}).toThrow();
|
||
});
|
||
|
||
it('应该处理传输层构造失败', () => {
|
||
// Mock传输层构造函数抛出异常
|
||
const originalWebSocket = (global as any).WebSocket;
|
||
(global as any).WebSocket = jest.fn(() => {
|
||
throw new Error('WebSocket不可用');
|
||
});
|
||
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
|
||
expect(client.connect()).rejects.toThrow();
|
||
|
||
// 恢复原始WebSocket
|
||
(global as any).WebSocket = originalWebSocket;
|
||
});
|
||
});
|
||
|
||
describe('功能测试', () => {
|
||
beforeEach(() => {
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
});
|
||
|
||
it('应该能够成功连接', async () => {
|
||
const connectPromise = client.connect();
|
||
|
||
// 模拟连接成功
|
||
setTimeout(() => {
|
||
const transport = (client as any).transport;
|
||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||
transport.websocket.readyState = WebSocket.OPEN;
|
||
transport.websocket.onopen(new Event('open'));
|
||
}
|
||
}, 0);
|
||
|
||
await expect(connectPromise).resolves.toBeUndefined();
|
||
});
|
||
|
||
it('应该能够发送消息', async () => {
|
||
// 先连接
|
||
const connectPromise = client.connect();
|
||
setTimeout(() => {
|
||
const transport = (client as any).transport;
|
||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||
transport.websocket.readyState = WebSocket.OPEN;
|
||
transport.websocket.onopen(new Event('open'));
|
||
}
|
||
}, 0);
|
||
await connectPromise;
|
||
|
||
// 发送消息
|
||
const message = {
|
||
type: 'custom' as const,
|
||
data: { test: 'message' },
|
||
reliable: true
|
||
};
|
||
|
||
// NetworkClient没有直接的sendMessage方法,它通过RPC调用
|
||
});
|
||
|
||
it('应该能够正确断开连接', async () => {
|
||
await expect(client.disconnect()).resolves.toBeUndefined();
|
||
});
|
||
|
||
it('应该返回正确的认证状态', () => {
|
||
expect(client.isAuthenticated()).toBe(false);
|
||
});
|
||
|
||
it('应该能够获取网络对象列表', () => {
|
||
const networkObjects = client.getAllNetworkObjects();
|
||
expect(Array.isArray(networkObjects)).toBe(true);
|
||
expect(networkObjects.length).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('消息ID生成测试', () => {
|
||
beforeEach(() => {
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
});
|
||
|
||
it('应该能够生成唯一的消息ID', () => {
|
||
const messageId1 = (client as any).generateMessageId();
|
||
const messageId2 = (client as any).generateMessageId();
|
||
|
||
expect(typeof messageId1).toBe('string');
|
||
expect(typeof messageId2).toBe('string');
|
||
expect(messageId1).not.toBe(messageId2);
|
||
});
|
||
|
||
it('生成的消息ID应该符合预期格式', () => {
|
||
const messageId = (client as any).generateMessageId();
|
||
|
||
// 检查消息ID格式(时间戳 + 随机字符串)
|
||
expect(messageId).toMatch(/^[a-z0-9]+$/);
|
||
expect(messageId.length).toBeGreaterThan(10);
|
||
});
|
||
});
|
||
|
||
describe('错误恢复测试', () => {
|
||
beforeEach(() => {
|
||
client = new NetworkClient({
|
||
transportType: 'websocket',
|
||
host: 'localhost',
|
||
port: 8080,
|
||
maxReconnectAttempts: 2,
|
||
reconnectInterval: 100
|
||
});
|
||
});
|
||
|
||
it('连接失败后应该尝试重连', async () => {
|
||
let connectAttempts = 0;
|
||
const originalWebSocket = (global as any).WebSocket;
|
||
|
||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||
connectAttempts++;
|
||
const ws = new originalWebSocket('ws://localhost:8080');
|
||
// 模拟连接失败
|
||
setTimeout(() => {
|
||
if (ws.onerror) {
|
||
ws.onerror(new Event('error'));
|
||
}
|
||
}, 0);
|
||
return ws;
|
||
});
|
||
|
||
await expect(client.connect()).rejects.toThrow();
|
||
|
||
// 等待重连尝试
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
|
||
expect(connectAttempts).toBeGreaterThan(1);
|
||
|
||
// 恢复原始WebSocket
|
||
(global as any).WebSocket = originalWebSocket;
|
||
});
|
||
|
||
it('达到最大重连次数后应该停止重连', async () => {
|
||
const maxAttempts = 2;
|
||
client = new NetworkClient({
|
||
transportType: 'websocket',
|
||
host: 'localhost',
|
||
port: 8080,
|
||
maxReconnectAttempts: maxAttempts,
|
||
reconnectInterval: 50
|
||
});
|
||
|
||
let connectAttempts = 0;
|
||
const originalWebSocket = (global as any).WebSocket;
|
||
|
||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||
connectAttempts++;
|
||
const ws = new originalWebSocket('ws://localhost:8080');
|
||
setTimeout(() => {
|
||
if (ws.onerror) {
|
||
ws.onerror(new Event('error'));
|
||
}
|
||
}, 0);
|
||
return ws;
|
||
});
|
||
|
||
await expect(client.connect()).rejects.toThrow();
|
||
|
||
// 等待所有重连尝试完成
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1);
|
||
|
||
// 恢复原始WebSocket
|
||
(global as any).WebSocket = originalWebSocket;
|
||
});
|
||
});
|
||
|
||
describe('内存泄漏防护测试', () => {
|
||
it('断开连接时应该清理所有资源', async () => {
|
||
client = new NetworkClient({
|
||
transport: 'websocket',
|
||
transportConfig: {
|
||
host: 'localhost',
|
||
port: 8080
|
||
}
|
||
});
|
||
|
||
const { Emitter } = require('@esengine/ecs-framework');
|
||
const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value;
|
||
|
||
await client.disconnect();
|
||
|
||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||
});
|
||
|
||
it('多次创建和销毁客户端不应该造成内存泄漏', () => {
|
||
const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||
|
||
// 创建和销毁多个客户端实例
|
||
for (let i = 0; i < 5; i++) {
|
||
const tempClient = new NetworkClient({
|
||
transportType: 'websocket',
|
||
host: 'localhost',
|
||
port: 8080
|
||
});
|
||
tempClient.disconnect().catch(() => {});
|
||
}
|
||
|
||
const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||
|
||
// 验证Emitter实例数量符合预期
|
||
expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5);
|
||
});
|
||
});
|
||
}); |