Endurance engineer! Consul Caesar.js needs to know whether our fort can withstand the toughest storms, longest campaigns, and most intense battles. Performance testing checks the endurance limits of our system!
Performance testing is like a stress test of the fort before setting out on a dangerous campaign:
1// Example of a simple performance test
2describe('Tribute Processing Performance', () => {
3 it('should process 1000 tributes within 500ms', async () => {
4 const tributes = Array.from({ length: 1000 }, (_, i) => ({
5 id: i,
6 name: `Tribute ${i}`,
7 value: Math.random() * 1000
8 }));
9
10 const startTime = performance.now();
11 const result = await tributeService.processBatch(tributes);
12 const endTime = performance.now();
13
14 const duration = endTime - startTime;
15
16 expect(duration).toBeLessThan(500);
17 expect(result.processed).toBe(1000);
18 expect(result.errors).toBe(0);
19 });
20});1// load-test.spec.ts
2describe('Normal Load Tests', () => {
3 let app: INestApplication;
4
5 beforeAll(async () => {
6 app = await createTestApp();
7 });
8
9 it('should handle 100 concurrent tribute requests', async () => {
10 const concurrentRequests = 100;
11 const requests = [];
12
13 // Prepare 100 concurrent requests
14 for (let i = 0; i < concurrentRequests; i++) {
15 requests.push(
16 request(app.getHttpServer())
17 .get('/tributes')
18 .expect(200)
19 );
20 }
21
22 const startTime = Date.now();
23 const responses = await Promise.allSettled(requests);
24 const endTime = Date.now();
25
26 const successfulRequests = responses.filter(r => r.status === 'fulfilled').length;
27 const averageResponseTime = (endTime - startTime) / concurrentRequests;
28
29 expect(successfulRequests).toBeGreaterThan(95); // 95% success rate
30 expect(averageResponseTime).toBeLessThan(100); // Average time < 100ms
31 });
32
33 it('should maintain database performance under load', async () => {
34 const operationsCount = 500;
35 const operations = [];
36
37 // Mix of different database operations
38 for (let i = 0; i < operationsCount; i++) {
39 if (i % 5 === 0) {
40 // 20% CREATE operations
41 operations.push(
42 tributeService.create({
43 name: `Load Test Tribute ${i}`,
44 value: Math.random() * 1000
45 })
46 );
47 } else if (i % 5 === 1) {
48 // 20% UPDATE operations
49 operations.push(
50 tributeService.update(i, { value: Math.random() * 1000 })
51 );
52 } else {
53 // 60% READ operations
54 operations.push(tributeService.findAll());
55 }
56 }
57
58 const startTime = Date.now();
59 const results = await Promise.allSettled(operations);
60 const endTime = Date.now();
61
62 const totalTime = endTime - startTime;
63 const successfulOperations = results.filter(r => r.status === 'fulfilled').length;
64 const throughput = successfulOperations / (totalTime / 1000); // ops/sec
65
66 expect(successfulOperations).toBeGreaterThan(475); // 95% success
67 expect(throughput).toBeGreaterThan(50); // min 50 ops/sec
68 expect(totalTime).toBeLessThan(10000); // max 10 seconds
69 });
70});1describe('Stress Tests', () => {
2 it('should find the breaking point of expedition service', async () => {
3 const results = [];
4 let currentLoad = 10;
5 let maxSuccessfulLoad = 0;
6
7 // Increase load until the system breaks
8 while (currentLoad <= 1000) {
9 const expeditions = Array.from({ length: currentLoad }, (_, i) => ({
10 destination: `Test Island ${i}`,
11 legionSize: 10,
12 duration: 1
13 }));
14
15 try {
16 const startTime = Date.now();
17 const promises = expeditions.map(exp =>
18 expeditionService.planExpedition(exp)
19 );
20
21 const responses = await Promise.allSettled(promises);
22 const endTime = Date.now();
23
24 const successCount = responses.filter(r => r.status === 'fulfilled').length;
25 const successRate = successCount / currentLoad;
26 const avgResponseTime = (endTime - startTime) / currentLoad;
27
28 results.push({
29 load: currentLoad,
30 successRate,
31 avgResponseTime,
32 totalTime: endTime - startTime
33 });
34
35 // If success rate dropped below 90% or response time > 5s
36 if (successRate < 0.9 || avgResponseTime > 5000) {
37 break;
38 }
39
40 maxSuccessfulLoad = currentLoad;
41 currentLoad = Math.floor(currentLoad * 1.5); // Increase by 50%
42
43 } catch (error) {
44 console.log(`System failed at load: ${currentLoad}`);
45 break;
46 }
47 }
48
49 console.log('Performance Results:', results);
50 console.log(`Maximum successful load: ${maxSuccessfulLoad}`);
51
52 expect(maxSuccessfulLoad).toBeGreaterThan(50); // System should handle at least 50
53 });
54
55 it('should handle memory pressure gracefully', async () => {
56 const initialMemory = process.memoryUsage().heapUsed;
57 const largeDataSets = [];
58
59 // Creating large data sets
60 for (let i = 0; i < 100; i++) {
61 const largeExpedition = {
62 id: i,
63 participants: Array.from({ length: 1000 }, (_, j) => ({
64 id: j,
65 name: `Legionary ${j}`,
66 skills: Array.from({ length: 50 }, () => Math.random()),
67 equipment: Array.from({ length: 20 }, () => `Item ${Math.random()}`)
68 })),
69 tributes: Array.from({ length: 500 }, (_, k) => ({
70 id: k,
71 name: `Tribute ${k}`,
72 description: 'A'.repeat(1000), // 1KB string
73 metadata: { /* complex object */ }
74 }))
75 };
76
77 largeDataSets.push(largeExpedition);
78
79 // Process data
80 await expeditionService.processExpeditionData(largeExpedition);
81
82 // Check memory usage
83 const currentMemory = process.memoryUsage().heapUsed;
84 const memoryIncrease = currentMemory - initialMemory;
85
86 // If memory increased by more than 100MB, run garbage collection
87 if (memoryIncrease > 100 * 1024 * 1024) {
88 global.gc?.();
89 }
90 }
91
92 // Force garbage collection
93 global.gc?.();
94
95 const finalMemory = process.memoryUsage().heapUsed;
96 const totalMemoryIncrease = finalMemory - initialMemory;
97
98 // Memory increase should be reasonable (< 50MB)
99 expect(totalMemoryIncrease).toBeLessThan(50 * 1024 * 1024);
100 });
101});1describe('Spike Tests', () => {
2 it('should handle sudden traffic spikes', async () => {
3 // Baseline - normal load
4 const baselineRequests = 10;
5 const baselineTime = await measureRequestTime(baselineRequests);
6
7 // Spike - sudden 1000% increase
8 const spikeRequests = 100;
9 const spikeStartTime = Date.now();
10
11 const spikePromises = Array.from({ length: spikeRequests }, () =>
12 request(app.getHttpServer())
13 .post('/expeditions/emergency')
14 .send({ type: 'rescue_mission', priority: 'high' })
15 );
16
17 const spikeResults = await Promise.allSettled(spikePromises);
18 const spikeEndTime = Date.now();
19
20 const successfulSpike = spikeResults.filter(r => r.status === 'fulfilled').length;
21 const spikeSuccessRate = successfulSpike / spikeRequests;
22 const spikeDuration = spikeEndTime - spikeStartTime;
23
24 // After spike - return to normal load
25 await new Promise(resolve => setTimeout(resolve, 2000)); // 2s recovery
26 const recoveryTime = await measureRequestTime(baselineRequests);
27
28 // Asserts
29 expect(spikeSuccessRate).toBeGreaterThan(0.8); // 80% requests should succeed
30 expect(spikeDuration).toBeLessThan(10000); // Spike processed in <10s
31 expect(recoveryTime).toBeLessThan(baselineTime * 1.5); // Recovery within 150% of baseline
32 });
33});
34
35async function measureRequestTime(requestCount: number): Promise<number> {
36 const startTime = Date.now();
37
38 const promises = Array.from({ length: requestCount }, () =>
39 request(app.getHttpServer()).get('/health').expect(200)
40 );
41
42 await Promise.all(promises);
43
44 return Date.now() - startTime;
45}1describe('Database Performance', () => {
2 let dataSource: DataSource;
3
4 beforeAll(async () => {
5 dataSource = app.get(DataSource);
6 });
7
8 it('should handle large batch operations efficiently', async () => {
9 const batchSize = 10000;
10 const tributes = Array.from({ length: batchSize }, (_, i) => ({
11 name: `Batch Tribute ${i}`,
12 value: Math.random() * 1000,
13 location: `Location ${i % 100}`,
14 collectedAt: new Date()
15 }));
16
17 // Test batch insert
18 const insertStartTime = Date.now();
19 await dataSource.getRepository(Tribute).save(tributes);
20 const insertTime = Date.now() - insertStartTime;
21
22 // Test batch read
23 const readStartTime = Date.now();
24 const allTributes = await dataSource.getRepository(Tribute).find();
25 const readTime = Date.now() - readStartTime;
26
27 // Test complex query
28 const queryStartTime = Date.now();
29 const expensiveTributes = await dataSource
30 .getRepository(Tribute)
31 .createQueryBuilder('tribute')
32 .where('tribute.value > :minValue', { minValue: 500 })
33 .orderBy('tribute.value', 'DESC')
34 .limit(100)
35 .getMany();
36 const queryTime = Date.now() - queryStartTime;
37
38 // Cleanup
39 await dataSource.getRepository(Tribute).delete({});
40
41 expect(insertTime).toBeLessThan(5000); // Insert 10k records in <5s
42 expect(readTime).toBeLessThan(1000); // Read all in <1s
43 expect(queryTime).toBeLessThan(100); // Complex query in <100ms
44 expect(allTributes.length).toBe(batchSize);
45 expect(expensiveTributes.length).toBeGreaterThan(0);
46 });
47
48 it('should maintain performance with concurrent connections', async () => {
49 const connectionCount = 20;
50 const operationsPerConnection = 50;
51
52 const connectionPromises = Array.from({ length: connectionCount }, async (_, i) => {
53 const operations = [];
54
55 for (let j = 0; j < operationsPerConnection; j++) {
56 operations.push(
57 dataSource.getRepository(Tribute).findOne({ where: { id: j + 1 } })
58 );
59 }
60
61 const startTime = Date.now();
62 await Promise.all(operations);
63 const endTime = Date.now();
64
65 return {
66 connectionId: i,
67 duration: endTime - startTime,
68 operationsCount: operationsPerConnection
69 };
70 });
71
72 const results = await Promise.all(connectionPromises);
73
74 const averageDuration = results.reduce((sum, r) => sum + r.duration, 0) / connectionCount;
75 const maxDuration = Math.max(...results.map(r => r.duration));
76
77 expect(averageDuration).toBeLessThan(2000); // Average < 2s
78 expect(maxDuration).toBeLessThan(5000); // Max < 5s
79 });
80});1describe('Resource Usage Tests', () => {
2 it('should not create memory leaks', async () => {
3 const initialMemory = process.memoryUsage();
4
5 // Simulate intensive work
6 for (let cycle = 0; cycle < 10; cycle++) {
7 const tributes = [];
8
9 // Create many objects
10 for (let i = 0; i < 1000; i++) {
11 tributes.push(await tributeService.create({
12 name: `Cycle ${cycle} Tribute ${i}`,
13 value: Math.random() * 1000
14 }));
15 }
16
17 // Process them
18 await tributeService.processBatch(tributes);
19
20 // Clean up
21 await tributeService.deleteMany(tributes.map(t => t.id));
22
23 // Force garbage collection every few cycles
24 if (cycle % 3 === 0) {
25 global.gc?.();
26 }
27 }
28
29 // Final cleanup
30 global.gc?.();
31
32 const finalMemory = process.memoryUsage();
33 const memoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
34
35 // Memory should not increase significantly
36 expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024); // < 10MB
37 });
38
39 it('should handle file system operations efficiently', async () => {
40 const fileCount = 100;
41 const fileSize = 1024; // 1KB per file
42
43 // Create test files
44 const createStartTime = Date.now();
45 const filePromises = Array.from({ length: fileCount }, (_, i) =>
46 fileService.createTributeMap(`map-${i}`, 'x'.repeat(fileSize))
47 );
48 await Promise.all(filePromises);
49 const createTime = Date.now() - createStartTime;
50
51 // Read all files
52 const readStartTime = Date.now();
53 const readPromises = Array.from({ length: fileCount }, (_, i) =>
54 fileService.readTributeMap(`map-${i}`)
55 );
56 const files = await Promise.all(readPromises);
57 const readTime = Date.now() - readStartTime;
58
59 // Delete all files
60 const deleteStartTime = Date.now();
61 const deletePromises = Array.from({ length: fileCount }, (_, i) =>
62 fileService.deleteTributeMap(`map-${i}`)
63 );
64 await Promise.all(deletePromises);
65 const deleteTime = Date.now() - deleteStartTime;
66
67 expect(createTime).toBeLessThan(2000); // Create 100 files in <2s
68 expect(readTime).toBeLessThan(1000); // Read 100 files in <1s
69 expect(deleteTime).toBeLessThan(1000); // Delete 100 files in <1s
70 expect(files.length).toBe(fileCount);
71 });
72});1// performance-regression.spec.ts
2describe('Performance Regression Tests', () => {
3 const performanceBaselines = {
4 tributeSearch: 50, // ms
5 expeditionPlanning: 200, // ms
6 legionAssignment: 100, // ms
7 batchProcessing: 1000, // ms for 1000 items
8 };
9
10 it('should not regress tribute search performance', async () => {
11 const iterations = 10;
12 const times = [];
13
14 for (let i = 0; i < iterations; i++) {
15 const startTime = performance.now();
16 await tributeService.search({
17 minValue: 100,
18 maxValue: 1000,
19 rarity: ['common', 'rare'],
20 location: 'Hispania'
21 });
22 const endTime = performance.now();
23
24 times.push(endTime - startTime);
25 }
26
27 const averageTime = times.reduce((sum, time) => sum + time, 0) / iterations;
28 const baseline = performanceBaselines.tributeSearch;
29
30 // Allow 10% performance degradation
31 const threshold = baseline * 1.1;
32
33 expect(averageTime).toBeLessThan(threshold);
34
35 if (averageTime > baseline) {
36 console.warn(`Performance regression detected: ${averageTime}ms vs baseline ${baseline}ms`);
37 }
38 });
39});1# .github/workflows/performance.yml
2name: Performance Tests
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8
9jobs:
10 performance:
11 runs-on: ubuntu-latest
12
13 steps:
14 - uses: actions/checkout@v3
15
16 - name: Setup Node.js
17 uses: actions/setup-node@v3
18 with:
19 node-version: '18'
20
21 - name: Install dependencies
22 run: npm ci
23
24 - name: Start test database
25 run: docker run -d -p 5432:5432 postgres:13
26
27 - name: Run performance tests
28 run: npm run test:performance
29
30 - name: Upload performance results
31 uses: actions/upload-artifact@v3
32 with:
33 name: performance-results
34 path: performance-results.jsonPerformance testing is like an endurance test for the fort - it shows whether it can survive the worst storms and toughest battles. Regular performance testing is a guarantee that your system will be ready for any challenge!