forked from datawhale/whale-town-end
test:添加位置广播系统端到端测试
- 添加并发用户测试场景 - 实现数据库恢复集成测试 - 重命名登录测试文件以符合命名规范
This commit is contained in:
515
test/location_broadcast/position_update.perf_spec.ts
Normal file
515
test/location_broadcast/position_update.perf_spec.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* 位置更新性能测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试位置更新的性能指标
|
||||
* - 验证系统在高负载下的表现
|
||||
* - 确保响应时间满足要求
|
||||
* - 提供性能基准数据
|
||||
*
|
||||
* 测试指标:
|
||||
* - 位置更新响应时间
|
||||
* - 并发用户处理能力
|
||||
* - 内存使用情况
|
||||
* - 系统吞吐量
|
||||
*
|
||||
* 最近修改:
|
||||
* - 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 '../../location_broadcast.module';
|
||||
import { RedisModule } from '../../../../core/redis/redis.module';
|
||||
import { LoginCoreModule } from '../../../../core/login_core/login_core.module';
|
||||
import { UserProfilesModule } from '../../../../core/db/user_profiles/user_profiles.module';
|
||||
|
||||
describe('位置更新性能测试', () => {
|
||||
let app: INestApplication;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
LocationBroadcastModule,
|
||||
RedisModule,
|
||||
LoginCoreModule,
|
||||
UserProfilesModule.forMemory(),
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
await app.listen(0);
|
||||
|
||||
authToken = 'test-jwt-token';
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('单用户位置更新性能', () => {
|
||||
let client: Socket;
|
||||
|
||||
beforeEach((done) => {
|
||||
const port = app.getHttpServer().address().port;
|
||||
client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
client.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId: 'perf-session-001',
|
||||
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
||||
});
|
||||
});
|
||||
|
||||
client.on('session_joined', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (client) {
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
it('应该在100ms内响应位置更新', (done) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
client.emit('position_update', {
|
||||
type: 'position_update',
|
||||
x: 150,
|
||||
y: 250,
|
||||
mapId: 'plaza',
|
||||
timestamp: startTime,
|
||||
});
|
||||
|
||||
client.on('position_update_success', () => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
console.log(`位置更新响应时间: ${responseTime}ms`);
|
||||
|
||||
expect(responseTime).toBeLessThan(100);
|
||||
done();
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持高频率位置更新', (done) => {
|
||||
const updateCount = 100;
|
||||
let completedUpdates = 0;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
const updateStartTime = Date.now();
|
||||
|
||||
client.emit('position_update', {
|
||||
type: 'position_update',
|
||||
x: 100 + i,
|
||||
y: 200 + i,
|
||||
mapId: 'plaza',
|
||||
timestamp: updateStartTime,
|
||||
});
|
||||
}
|
||||
|
||||
client.on('position_update_success', () => {
|
||||
completedUpdates++;
|
||||
|
||||
if (completedUpdates === updateCount) {
|
||||
const totalTime = Date.now() - startTime;
|
||||
const avgTime = totalTime / updateCount;
|
||||
|
||||
console.log(`${updateCount}次位置更新总耗时: ${totalTime}ms`);
|
||||
console.log(`平均每次更新耗时: ${avgTime}ms`);
|
||||
console.log(`更新频率: ${(updateCount / totalTime * 1000).toFixed(2)} updates/sec`);
|
||||
|
||||
expect(avgTime).toBeLessThan(50); // 平均响应时间应小于50ms
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
|
||||
// 超时保护
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (completedUpdates < updateCount) {
|
||||
done(new Error(`只完成了 ${completedUpdates}/${updateCount} 次更新`));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('多用户并发性能', () => {
|
||||
it('应该支持100个并发用户', (done) => {
|
||||
const userCount = 100;
|
||||
const clients: Socket[] = [];
|
||||
const sessionId = 'perf-session-concurrent';
|
||||
let connectedUsers = 0;
|
||||
let joinedUsers = 0;
|
||||
let updateResponses = 0;
|
||||
|
||||
const port = app.getHttpServer().address().port;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 创建多个客户端连接
|
||||
for (let i = 0; i < userCount; i++) {
|
||||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
clients.push(client);
|
||||
|
||||
client.on('connect', () => {
|
||||
connectedUsers++;
|
||||
|
||||
if (connectedUsers === userCount) {
|
||||
const connectTime = Date.now() - startTime;
|
||||
console.log(`${userCount}个用户连接耗时: ${connectTime}ms`);
|
||||
|
||||
// 所有用户加入会话
|
||||
clients.forEach((c, index) => {
|
||||
c.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId,
|
||||
initialPosition: {
|
||||
x: 100 + index,
|
||||
y: 200 + index,
|
||||
mapId: 'plaza',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
client.on('session_joined', () => {
|
||||
joinedUsers++;
|
||||
|
||||
if (joinedUsers === userCount) {
|
||||
const joinTime = Date.now() - startTime;
|
||||
console.log(`${userCount}个用户加入会话耗时: ${joinTime}ms`);
|
||||
|
||||
// 所有用户同时更新位置
|
||||
clients.forEach((c, index) => {
|
||||
c.emit('position_update', {
|
||||
type: 'position_update',
|
||||
x: 200 + index,
|
||||
y: 300 + index,
|
||||
mapId: 'plaza',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
client.on('position_update_success', () => {
|
||||
updateResponses++;
|
||||
|
||||
if (updateResponses === userCount) {
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`${userCount}个用户完整流程耗时: ${totalTime}ms`);
|
||||
console.log(`平均每用户耗时: ${(totalTime / userCount).toFixed(2)}ms`);
|
||||
|
||||
// 清理连接
|
||||
clients.forEach(c => c.disconnect());
|
||||
|
||||
expect(totalTime).toBeLessThan(10000); // 总时间应小于10秒
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
clients.forEach(c => c.disconnect());
|
||||
done(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
const timeoutId = setTimeout(() => {
|
||||
clients.forEach(c => c.disconnect());
|
||||
done(new Error(`测试超时,连接用户: ${connectedUsers}, 加入用户: ${joinedUsers}, 更新响应: ${updateResponses}`));
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
it('应该支持持续的位置广播', (done) => {
|
||||
const userCount = 10;
|
||||
const updatesPerUser = 50;
|
||||
const clients: Socket[] = [];
|
||||
const sessionId = 'perf-session-broadcast';
|
||||
let totalBroadcasts = 0;
|
||||
let expectedBroadcasts = userCount * updatesPerUser * (userCount - 1); // 每次更新广播给其他用户
|
||||
|
||||
const port = app.getHttpServer().address().port;
|
||||
const startTime = Date.now();
|
||||
|
||||
// 创建多个客户端
|
||||
for (let i = 0; i < userCount; i++) {
|
||||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
clients.push(client);
|
||||
|
||||
client.on('connect', () => {
|
||||
client.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId,
|
||||
initialPosition: {
|
||||
x: 100 + i * 10,
|
||||
y: 200 + i * 10,
|
||||
mapId: 'plaza',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
client.on('position_broadcast', () => {
|
||||
totalBroadcasts++;
|
||||
|
||||
if (totalBroadcasts >= expectedBroadcasts * 0.8) { // 允许80%的广播成功
|
||||
const totalTime = Date.now() - startTime;
|
||||
const broadcastRate = totalBroadcasts / totalTime * 1000;
|
||||
|
||||
console.log(`广播测试完成: ${totalBroadcasts}/${expectedBroadcasts} 条广播`);
|
||||
console.log(`总耗时: ${totalTime}ms`);
|
||||
console.log(`广播频率: ${broadcastRate.toFixed(2)} broadcasts/sec`);
|
||||
|
||||
clients.forEach(c => c.disconnect());
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 等待所有用户连接后开始更新
|
||||
const startUpdateTimer = setTimeout(() => {
|
||||
clients.forEach((client, userIndex) => {
|
||||
for (let updateIndex = 0; updateIndex < updatesPerUser; updateIndex++) {
|
||||
const updateTimer = setTimeout(() => {
|
||||
client.emit('position_update', {
|
||||
type: 'position_update',
|
||||
x: 100 + userIndex * 10 + updateIndex,
|
||||
y: 200 + userIndex * 10 + updateIndex,
|
||||
mapId: 'plaza',
|
||||
});
|
||||
}, updateIndex * 10); // 每10ms发送一次更新
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// 超时保护
|
||||
const timeoutId = setTimeout(() => {
|
||||
clients.forEach(c => c.disconnect());
|
||||
console.log(`测试超时,收到广播: ${totalBroadcasts}/${expectedBroadcasts}`);
|
||||
done();
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存和资源使用', () => {
|
||||
it('应该在合理范围内使用内存', async () => {
|
||||
const initialMemory = process.memoryUsage();
|
||||
const userCount = 50;
|
||||
const clients: Socket[] = [];
|
||||
const port = app.getHttpServer().address().port;
|
||||
|
||||
// 创建多个连接
|
||||
for (let i = 0; i < userCount; i++) {
|
||||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
// 等待连接建立
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const peakMemory = process.memoryUsage();
|
||||
const memoryIncrease = peakMemory.heapUsed - initialMemory.heapUsed;
|
||||
const memoryPerUser = memoryIncrease / userCount;
|
||||
|
||||
console.log(`初始内存使用: ${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`峰值内存使用: ${(peakMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`内存增长: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`每用户内存: ${(memoryPerUser / 1024).toFixed(2)} KB`);
|
||||
|
||||
// 清理连接
|
||||
clients.forEach(c => c.disconnect());
|
||||
|
||||
// 等待清理完成
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const finalMemory = process.memoryUsage();
|
||||
console.log(`清理后内存: ${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
// 每个用户的内存使用应该小于100KB
|
||||
expect(memoryPerUser).toBeLessThan(100 * 1024);
|
||||
});
|
||||
|
||||
it('应该正确清理断开连接的用户', (done) => {
|
||||
const port = app.getHttpServer().address().port;
|
||||
const sessionId = 'cleanup-test-session';
|
||||
|
||||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
client.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId,
|
||||
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
||||
});
|
||||
});
|
||||
|
||||
client.on('session_joined', () => {
|
||||
// 突然断开连接
|
||||
client.disconnect();
|
||||
|
||||
// 等待系统清理
|
||||
setTimeout(() => {
|
||||
// 创建新连接验证清理是否成功
|
||||
const newClient = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
newClient.on('connect', () => {
|
||||
newClient.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId,
|
||||
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
||||
});
|
||||
});
|
||||
|
||||
newClient.on('session_joined', (response) => {
|
||||
// 如果能成功加入,说明之前的用户已被清理
|
||||
expect(response.success).toBe(true);
|
||||
newClient.disconnect();
|
||||
done();
|
||||
});
|
||||
|
||||
newClient.on('error', (error) => {
|
||||
newClient.disconnect();
|
||||
done(error);
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('压力测试', () => {
|
||||
it('应该在高频率更新下保持稳定', (done) => {
|
||||
const port = app.getHttpServer().address().port;
|
||||
const sessionId = 'stress-test-session';
|
||||
const updateInterval = 10; // 10ms间隔
|
||||
const testDuration = 5000; // 5秒测试
|
||||
|
||||
let updateCount = 0;
|
||||
let responseCount = 0;
|
||||
let errorCount = 0;
|
||||
let updateTimer: NodeJS.Timeout | null = null;
|
||||
let timeoutTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const client = io(`http://localhost:${port}/location-broadcast`, {
|
||||
auth: { token: authToken },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
if (updateTimer) {
|
||||
clearInterval(updateTimer);
|
||||
updateTimer = null;
|
||||
}
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
}
|
||||
if (client && client.connected) {
|
||||
client.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
client.on('connect', () => {
|
||||
client.emit('join_session', {
|
||||
type: 'join_session',
|
||||
sessionId,
|
||||
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
||||
});
|
||||
});
|
||||
|
||||
client.on('session_joined', () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
updateTimer = setInterval(() => {
|
||||
if (Date.now() - startTime >= testDuration) {
|
||||
clearInterval(updateTimer!);
|
||||
updateTimer = null;
|
||||
|
||||
// 等待最后的响应
|
||||
setTimeout(() => {
|
||||
const successRate = (responseCount / updateCount) * 100;
|
||||
const errorRate = (errorCount / updateCount) * 100;
|
||||
|
||||
console.log(`压力测试结果:`);
|
||||
console.log(`- 发送更新: ${updateCount}`);
|
||||
console.log(`- 成功响应: ${responseCount}`);
|
||||
console.log(`- 错误数量: ${errorCount}`);
|
||||
console.log(`- 成功率: ${successRate.toFixed(2)}%`);
|
||||
console.log(`- 错误率: ${errorRate.toFixed(2)}%`);
|
||||
|
||||
cleanup();
|
||||
|
||||
// 成功率应该大于95%
|
||||
expect(successRate).toBeGreaterThan(95);
|
||||
done();
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
updateCount++;
|
||||
client.emit('position_update', {
|
||||
type: 'position_update',
|
||||
x: 100 + (updateCount % 100),
|
||||
y: 200 + (updateCount % 100),
|
||||
mapId: 'plaza',
|
||||
});
|
||||
}, updateInterval);
|
||||
});
|
||||
|
||||
client.on('position_update_success', () => {
|
||||
responseCount++;
|
||||
});
|
||||
|
||||
client.on('error', () => {
|
||||
errorCount++;
|
||||
});
|
||||
|
||||
// 超时保护
|
||||
timeoutTimer = setTimeout(() => {
|
||||
cleanup();
|
||||
done(new Error('压力测试超时'));
|
||||
}, testDuration + 5000);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user