306 lines
8.2 KiB
TypeScript
306 lines
8.2 KiB
TypeScript
/**
|
||
* 并发用户测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试系统在多用户并发场景下的正确性和稳定性
|
||
* - 验证并发位置更新的数据一致性
|
||
* - 确保会话管理在高并发下的可靠性
|
||
* - 测试系统的并发处理能力和性能表现
|
||
*
|
||
* 测试场景:
|
||
* - 大量用户同时连接和断开
|
||
* - 多用户同时加入/离开会话
|
||
* - 并发位置更新和广播
|
||
* - 数据竞争和一致性验证
|
||
* - 系统资源使用和清理
|
||
*
|
||
* 验证属性:
|
||
* - Property 17: Concurrent update handling (并发更新处理)
|
||
* - Property 5: Position storage consistency (位置存储一致性)
|
||
* - Property 8: Session-scoped broadcasting (会话范围广播)
|
||
* - Property 1: User session membership consistency (用户会话成员一致性)
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-08: 文件重命名 - 修正kebab-case为snake_case命名规范 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.1
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-08
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { INestApplication } from '@nestjs/common';
|
||
import { io, Socket } from 'socket.io-client';
|
||
import { LocationBroadcastModule } from '../../src/business/location_broadcast/location_broadcast.module';
|
||
|
||
interface TestUser {
|
||
id: string;
|
||
client: Socket;
|
||
sessionId?: string;
|
||
position?: { x: number; y: number; mapId: string };
|
||
connected: boolean;
|
||
joined: boolean;
|
||
}
|
||
|
||
describe('并发用户测试', () => {
|
||
let app: INestApplication;
|
||
let authToken: string;
|
||
let port: number;
|
||
let activeTimers: Set<NodeJS.Timeout> = new Set();
|
||
|
||
// 全局定时器管理
|
||
const createTimer = (callback: () => void, delay: number): NodeJS.Timeout => {
|
||
const timer = setTimeout(() => {
|
||
activeTimers.delete(timer);
|
||
callback();
|
||
}, delay);
|
||
activeTimers.add(timer);
|
||
return timer;
|
||
};
|
||
|
||
const clearTimer = (timer: NodeJS.Timeout): void => {
|
||
clearTimeout(timer);
|
||
activeTimers.delete(timer);
|
||
};
|
||
|
||
const clearAllTimers = (): void => {
|
||
activeTimers.forEach(timer => clearTimeout(timer));
|
||
activeTimers.clear();
|
||
};
|
||
|
||
beforeAll(async () => {
|
||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||
imports: [LocationBroadcastModule],
|
||
}).compile();
|
||
|
||
app = moduleFixture.createNestApplication();
|
||
await app.init();
|
||
await app.listen(0);
|
||
port = app.getHttpServer().address().port;
|
||
|
||
authToken = 'test-jwt-token';
|
||
});
|
||
|
||
afterAll(async () => {
|
||
clearAllTimers();
|
||
await app.close();
|
||
});
|
||
|
||
afterEach(() => {
|
||
clearAllTimers();
|
||
});
|
||
|
||
/**
|
||
* 创建测试用户连接
|
||
*/
|
||
const createTestUser = (userId: string): Promise<TestUser> => {
|
||
return new Promise((resolve, reject) => {
|
||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||
auth: { token: authToken },
|
||
transports: ['websocket'],
|
||
});
|
||
|
||
const user: TestUser = {
|
||
id: userId,
|
||
client,
|
||
connected: false,
|
||
joined: false,
|
||
};
|
||
|
||
let timeoutId: NodeJS.Timeout | null = null;
|
||
|
||
client.on('connect', () => {
|
||
user.connected = true;
|
||
if (timeoutId) {
|
||
clearTimer(timeoutId);
|
||
timeoutId = null;
|
||
}
|
||
resolve(user);
|
||
});
|
||
|
||
client.on('connect_error', (error) => {
|
||
if (timeoutId) {
|
||
clearTimer(timeoutId);
|
||
timeoutId = null;
|
||
}
|
||
reject(error);
|
||
});
|
||
|
||
// 超时保护
|
||
timeoutId = createTimer(() => {
|
||
if (!user.connected) {
|
||
client.disconnect();
|
||
reject(new Error('Connection timeout'));
|
||
}
|
||
timeoutId = null;
|
||
}, 5000);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 用户加入会话
|
||
*/
|
||
const joinSession = (user: TestUser, sessionId: string, initialPosition?: { x: number; y: number; mapId: string }): Promise<void> => {
|
||
return new Promise((resolve, reject) => {
|
||
user.sessionId = sessionId;
|
||
user.position = initialPosition || { x: Math.random() * 1000, y: Math.random() * 1000, mapId: 'plaza' };
|
||
|
||
let timeoutId: NodeJS.Timeout | null = null;
|
||
|
||
user.client.emit('join_session', {
|
||
type: 'join_session',
|
||
sessionId,
|
||
initialPosition: user.position,
|
||
});
|
||
|
||
user.client.on('session_joined', () => {
|
||
user.joined = true;
|
||
if (timeoutId) {
|
||
clearTimer(timeoutId);
|
||
timeoutId = null;
|
||
}
|
||
resolve();
|
||
});
|
||
|
||
user.client.on('error', (error) => {
|
||
if (timeoutId) {
|
||
clearTimer(timeoutId);
|
||
timeoutId = null;
|
||
}
|
||
reject(error);
|
||
});
|
||
|
||
// 超时保护
|
||
timeoutId = createTimer(() => {
|
||
if (!user.joined) {
|
||
reject(new Error('Join session timeout'));
|
||
}
|
||
timeoutId = null;
|
||
}, 5000);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 清理用户连接
|
||
*/
|
||
const cleanupUsers = (users: TestUser[]) => {
|
||
users.forEach(user => {
|
||
if (user.client && user.client.connected) {
|
||
user.client.disconnect();
|
||
}
|
||
});
|
||
};
|
||
|
||
describe('大规模并发连接测试', () => {
|
||
it('应该支持100个用户同时连接', async () => {
|
||
const userCount = 100;
|
||
const users: TestUser[] = [];
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
// 并发创建用户连接
|
||
const connectionPromises = Array.from({ length: userCount }, (_, i) =>
|
||
createTestUser(`concurrent-user-${i}`)
|
||
);
|
||
|
||
const connectedUsers = await Promise.all(connectionPromises);
|
||
users.push(...connectedUsers);
|
||
|
||
const connectionTime = Date.now() - startTime;
|
||
console.log(`${userCount} users connected in ${connectionTime}ms`);
|
||
console.log(`Average connection time: ${(connectionTime / userCount).toFixed(2)}ms per user`);
|
||
|
||
// 验证所有用户都已连接
|
||
expect(users.length).toBe(userCount);
|
||
users.forEach(user => {
|
||
expect(user.connected).toBe(true);
|
||
expect(user.client.connected).toBe(true);
|
||
});
|
||
|
||
// 连接时间应该在合理范围内(每个用户平均不超过100ms)
|
||
expect(connectionTime / userCount).toBeLessThan(100);
|
||
|
||
} finally {
|
||
cleanupUsers(users);
|
||
}
|
||
}, 30000);
|
||
|
||
it('应该支持用户快速连接和断开', async () => {
|
||
const userCount = 50;
|
||
const users: TestUser[] = [];
|
||
|
||
try {
|
||
// 快速连接
|
||
for (let i = 0; i < userCount; i++) {
|
||
const user = await createTestUser(`rapid-user-${i}`);
|
||
users.push(user);
|
||
|
||
// 立即断开一半用户
|
||
if (i % 2 === 0) {
|
||
user.client.disconnect();
|
||
user.connected = false;
|
||
}
|
||
}
|
||
|
||
// 等待系统处理断开连接
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
// 验证剩余用户仍然连接
|
||
const connectedUsers = users.filter(user => user.connected);
|
||
expect(connectedUsers.length).toBe(userCount / 2);
|
||
|
||
connectedUsers.forEach(user => {
|
||
expect(user.client.connected).toBe(true);
|
||
});
|
||
|
||
} finally {
|
||
cleanupUsers(users);
|
||
}
|
||
}, 20000);
|
||
});
|
||
|
||
describe('并发会话管理测试', () => {
|
||
it('应该支持多用户同时加入同一会话', async () => {
|
||
const userCount = 50;
|
||
const sessionId = 'concurrent-session-001';
|
||
const users: TestUser[] = [];
|
||
|
||
try {
|
||
// 创建用户连接
|
||
for (let i = 0; i < userCount; i++) {
|
||
const user = await createTestUser(`session-user-${i}`);
|
||
users.push(user);
|
||
}
|
||
|
||
const startTime = Date.now();
|
||
|
||
// 并发加入会话
|
||
const joinPromises = users.map(user =>
|
||
joinSession(user, sessionId, {
|
||
x: Math.random() * 1000,
|
||
y: Math.random() * 1000,
|
||
mapId: 'plaza'
|
||
})
|
||
);
|
||
|
||
await Promise.all(joinPromises);
|
||
|
||
const joinTime = Date.now() - startTime;
|
||
console.log(`${userCount} users joined session in ${joinTime}ms`);
|
||
|
||
// 验证所有用户都成功加入会话
|
||
users.forEach(user => {
|
||
expect(user.joined).toBe(true);
|
||
expect(user.sessionId).toBe(sessionId);
|
||
});
|
||
|
||
// 加入时间应该在合理范围内
|
||
expect(joinTime).toBeLessThan(10000);
|
||
|
||
} finally {
|
||
cleanupUsers(users);
|
||
}
|
||
}, 30000);
|
||
});
|
||
}); |