Files
whale-town-end/test/location_broadcast/concurrent_users.e2e_spec.ts
moyin 71bc317c57 test:添加位置广播系统端到端测试
- 添加并发用户测试场景
- 实现数据库恢复集成测试
- 重命名登录测试文件以符合命名规范
2026-01-08 23:06:11 +08:00

306 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 并发用户测试
*
* 功能描述:
* - 测试系统在多用户并发场景下的正确性和稳定性
* - 验证并发位置更新的数据一致性
* - 确保会话管理在高并发下的可靠性
* - 测试系统的并发处理能力和性能表现
*
* 测试场景:
* - 大量用户同时连接和断开
* - 多用户同时加入/离开会话
* - 并发位置更新和广播
* - 数据竞争和一致性验证
* - 系统资源使用和清理
*
* 验证属性:
* - 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);
});
});