forked from datawhale/whale-town-end
test:添加位置广播系统端到端测试
- 添加并发用户测试场景 - 实现数据库恢复集成测试 - 重命名登录测试文件以符合命名规范
This commit is contained in:
306
test/location_broadcast/concurrent_users.e2e_spec.ts
Normal file
306
test/location_broadcast/concurrent_users.e2e_spec.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 并发用户测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试系统在多用户并发场景下的正确性和稳定性
|
||||
* - 验证并发位置更新的数据一致性
|
||||
* - 确保会话管理在高并发下的可靠性
|
||||
* - 测试系统的并发处理能力和性能表现
|
||||
*
|
||||
* 测试场景:
|
||||
* - 大量用户同时连接和断开
|
||||
* - 多用户同时加入/离开会话
|
||||
* - 并发位置更新和广播
|
||||
* - 数据竞争和一致性验证
|
||||
* - 系统资源使用和清理
|
||||
*
|
||||
* 验证属性:
|
||||
* - 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user