We use cookies to enhance your experience on the site
CodeWorlds

Integration Testing - Checking Legion Cooperation

Expedition coordinator! Consul Caesar.js noticed that although every element of the fort works perfectly on its own, sometimes unexpected problems arise when they have to work together. It's time to learn integration testing - checking whether different parts of the legion can work together like a well-oiled machine!

What Is Integration Testing?

Imagine your fort is an orchestra. Every musician (unit) plays their part perfectly, but will they create a beautiful melody together, or a cacophony? Integration testing checks:

  • Whether the speculator cooperates with the legate in route planning
  • Whether the gunners coordinate fire with the commander's orders
  • Whether the communication system efficiently transfers information between the front lines
  • Whether the database correctly saves operation results
1// Example of a problem detected by integration testing
2describe('Tribute Hunt Coordination', () => {
3 it('should coordinate the entire tribute expedition', async () => {
4 // These components must cooperate
5 const speculator = new NavigationService();
6 const tributeDetector = new TributeDetectionService();
7 const legionManager = new LegionManagementService();
8 const expeditionService = new ExpeditionService(speculator, tributeDetector, legionManager);
9
10 const result = await expeditionService.launchTributeHunt({
11 destination: 'Ostia Island',
12 legionSize: 15,
13 maxDuration: 7
14 });
15
16 expect(result.success).toBe(true);
17 expect(result.tributesFound).toBeGreaterThan(0);
18 expect(result.legionStatus).toBe('healthy');
19 });
20});

Integration Testing Strategies in NestJS

1. Module Integration Testing

Testing how different modules cooperate with each other:

1// tribute-expedition.integration.spec.ts
2describe('Tribute Expedition Integration', () => {
3 let app: TestingModule;
4 let tributeService: TributeService;
5 let legionService: LegionService;
6 let expeditionService: ExpeditionService;
7
8 beforeEach(async () => {
9 app = await Test.createTestingModule({
10 imports: [
11 TypeOrmModule.forRoot({
12 type: 'sqlite',
13 database: ':memory:',
14 entities: [Tribute, Legionary, Expedition],
15 synchronize: true,
16 }),
17 TributeModule,
18 LegionModule,
19 ExpeditionModule,
20 ],
21 }).compile();
22
23 tributeService = app.get<TributeService>(TributeService);
24 legionService = app.get<LegionService>(LegionService);
25 expeditionService = app.get<ExpeditionService>(ExpeditionService);
26 });
27
28 afterEach(async () => {
29 await app.close();
30 });
31
32 describe('Complete expedition flow', () => {
33 it('should conduct a full expedition from planning to tribute distribution', async () => {
34 // 1. Prepare the cohort
35 const centurion = await legionService.recruitLegionary({
36 name: 'Centurion Brutus',
37 rank: 'Centurion',
38 skills: ['Navigation', 'Leadership']
39 });
40
41 const legion = await Promise.all([
42 legionService.recruitLegionary({ name: 'Smee', rank: 'Optio', skills: ['Combat'] }),
43 legionService.recruitLegionary({ name: 'Bill Jukes', rank: 'Ballistarius', skills: ['Artillery'] }),
44 legionService.recruitLegionary({ name: 'Noodler', rank: 'Legionary', skills: ['Marching'] }),
45 ]);
46
47 // 2. Plan the expedition
48 const expeditionPlan = await expeditionService.planExpedition({
49 centurionId: centurion.id,
50 legionIds: legion.map(p => p.id),
51 destination: { lat: 25.7617, lng: -80.1918 }, // Coordinates of tribute island
52 estimatedDuration: 5
53 });
54
55 expect(expeditionPlan.feasible).toBe(true);
56 expect(expeditionPlan.legionReadiness).toBeGreaterThan(0.8);
57
58 // 3. Execute the expedition
59 const expeditionResult = await expeditionService.executeExpedition(expeditionPlan.id);
60
61 expect(expeditionResult.status).toBe('completed');
62 expect(expeditionResult.tributesDiscovered).toHaveLength.toBeGreaterThan(0);
63
64 // 4. Check if tributes were added to the database
65 const allTributes = await tributeService.findAll();
66 const expeditionTributes = allTributes.filter(t => t.expeditionId === expeditionPlan.id);
67
68 expect(expeditionTributes.length).toBe(expeditionResult.tributesDiscovered.length);
69
70 // 5. Check if the legion received experience
71 const updatedLegion = await Promise.all(
72 legion.map(p => legionService.findById(p.id))
73 );
74
75 updatedLegion.forEach(legionaries => {
76 expect(legionaries.experience).toBeGreaterThan(legion.find(c => c.id === legionaries.id).experience);
77 });
78
79 // 6. Check tribute distribution
80 const tributeDivision = await expeditionService.divideTributes(expeditionPlan.id);
81
82 expect(tributeDivision.centurionShare).toBeGreaterThan(tributeDivision.legionShare);
83 expect(tributeDivision.totalValue).toBe(
84 expeditionResult.tributesDiscovered.reduce((sum, t) => sum + t.value, 0)
85 );
86 });
87
88 it('should handle expedition failure', async () => {
89 const centurion = await legionService.recruitLegionary({
90 name: 'Inexperienced Centurion',
91 rank: 'Centurion',
92 skills: [], // No skills
93 experience: 0
94 });
95
96 const expeditionPlan = await expeditionService.planExpedition({
97 centurionId: centurion.id,
98 legionIds: [],
99 destination: { lat: 0, lng: 0 }, // Dangerous location
100 estimatedDuration: 30 // Too long
101 });
102
103 expect(expeditionPlan.feasible).toBe(false);
104 expect(expeditionPlan.risks).toContain('inexperienced_centurion');
105 expect(expeditionPlan.risks).toContain('insufficient_legion');
106 });
107 });
108});

2. Database Integration Testing

Testing cooperation with a real database:

1// database-integration.spec.ts
2describe('Database Integration', () => {
3 let app: TestingModule;
4 let dataSource: DataSource;
5 let tributeRepository: Repository<Tribute>;
6 let legionariesRepository: Repository<Legionary>;
7
8 beforeEach(async () => {
9 app = await Test.createTestingModule({
10 imports: [
11 TypeOrmModule.forRoot({
12 type: 'postgres',
13 host: process.env.TEST_DB_HOST || 'localhost',
14 port: parseInt(process.env.TEST_DB_PORT) || 5432,
15 username: process.env.TEST_DB_USER || 'test',
16 password: process.env.TEST_DB_PASS || 'test',
17 database: process.env.TEST_DB_NAME || 'legionaries_test',
18 entities: [Tribute, Legionary, Legion],
19 synchronize: true,
20 dropSchema: true, // Clear database before each test
21 }),
22 TributeModule,
23 LegionModule,
24 ],
25 }).compile();
26
27 dataSource = app.get<DataSource>(DataSource);
28 tributeRepository = dataSource.getRepository(Tribute);
29 legionariesRepository = dataSource.getRepository(Legionary);
30 });
31
32 afterEach(async () => {
33 await app.close();
34 });
35
36 describe('Transaction handling', () => {
37 it('should execute a tribute transfer transaction', async () => {
38 const queryRunner = dataSource.createQueryRunner();
39 await queryRunner.connect();
40 await queryRunner.startTransaction();
41
42 try {
43 // Create legionaries
44 const legionaries1 = await queryRunner.manager.save(Legionary, {
45 name: 'Marcus Aquila',
46 rank: 'Centurion',
47 gold: 1000
48 });
49
50 const legionaries2 = await queryRunner.manager.save(Legionary, {
51 name: 'Will Turner',
52 rank: 'Legionary',
53 gold: 100
54 });
55
56 // Create tribute
57 const tribute = await queryRunner.manager.save(Tribute, {
58 name: 'Golden Compass',
59 value: 500,
60 ownerId: legionaries1.id
61 });
62
63 // Transfer tribute
64 await queryRunner.manager.update(Tribute, tribute.id, {
65 ownerId: legionaries2.id
66 });
67
68 await queryRunner.manager.update(Legionary, legionaries1.id, {
69 gold: legionaries1.gold + tribute.value
70 });
71
72 await queryRunner.manager.update(Legionary, legionaries2.id, {
73 gold: legionaries2.gold - tribute.value
74 });
75
76 await queryRunner.commitTransaction();
77
78 // Check results
79 const updatedTribute = await tributeRepository.findOne({
80 where: { id: tribute.id }
81 });
82 const updatedLegionary1 = await legionariesRepository.findOne({
83 where: { id: legionaries1.id }
84 });
85 const updatedLegionary2 = await legionariesRepository.findOne({
86 where: { id: legionaries2.id }
87 });
88
89 expect(updatedTribute.ownerId).toBe(legionaries2.id);
90 expect(updatedLegionary1.gold).toBe(1500);
91 expect(updatedLegionary2.gold).toBe(-400);
92
93 } catch (error) {
94 await queryRunner.rollbackTransaction();
95 throw error;
96 } finally {
97 await queryRunner.release();
98 }
99 });
100
101 it('should rollback transaction on error', async () => {
102 const queryRunner = dataSource.createQueryRunner();
103 await queryRunner.connect();
104 await queryRunner.startTransaction();
105
106 try {
107 const legionaries = await queryRunner.manager.save(Legionary, {
108 name: 'Test Legionary',
109 rank: 'Legionary',
110 gold: 100
111 });
112
113 // Simulate error
114 await queryRunner.manager.query('INVALID SQL QUERY');
115
116 await queryRunner.commitTransaction();
117 } catch (error) {
118 await queryRunner.rollbackTransaction();
119
120 // Check that the legionary was not saved
121 const legionaries = await legionariesRepository.find({ where: { name: 'Test Legionary' } });
122 expect(legionaries.length).toBe(0);
123 } finally {
124 await queryRunner.release();
125 }
126 });
127 });
128
129 describe('Complex queries', () => {
130 it('should execute a complex query with JOIN', async () => {
131 // Create test data
132 const cohort = await dataSource.getRepository(Legion).save({
133 name: 'Cohors Nigra',
134 capacity: 50
135 });
136
137 const centurion = await legionariesRepository.save({
138 name: 'Caesar',
139 rank: 'Centurion',
140 cohortId: cohort.id
141 });
142
143 const tributes = await tributeRepository.save([
144 { name: 'Gold Coin', value: 100, ownerId: centurion.id },
145 { name: 'Silver Ring', value: 50, ownerId: centurion.id },
146 { name: 'Ruby Necklace', value: 300, ownerId: centurion.id },
147 ]);
148
149 // Execute complex query
150 const result = await dataSource
151 .getRepository(Legionary)
152 .createQueryBuilder('legionaries')
153 .leftJoinAndSelect('legionaries.cohort', 'cohort')
154 .leftJoin('legionaries.tributes', 'tribute')
155 .select([
156 'legionaries.name as userName',
157 'legionaries.rank as rank',
158 'cohort.name as cohortName',
159 'COUNT(tribute.id) as tributeCount',
160 'SUM(tribute.value) as totalValue'
161 ])
162 .where('legionaries.rank = :rank', { rank: 'Centurion' })
163 .groupBy('legionaries.id, cohort.id')
164 .getRawOne();
165
166 expect(result.userName).toBe('Caesar');
167 expect(result.cohortName).toBe('Cohors Nigra');
168 expect(parseInt(result.tributeCount)).toBe(3);
169 expect(parseInt(result.totalValue)).toBe(450);
170 });
171 });
172});

3. HTTP Integration Testing

Testing the full HTTP request flow:

1// http-integration.spec.ts
2describe('HTTP Integration', () => {
3 let app: INestApplication;
4 let httpService: HttpService;
5
6 beforeEach(async () => {
7 const moduleFixture = await Test.createTestingModule({
8 imports: [
9 HttpModule,
10 ConfigModule.forRoot({
11 isGlobal: true,
12 envFilePath: '.env.test',
13 }),
14 TributeModule,
15 LegionModule,
16 ],
17 }).compile();
18
19 app = moduleFixture.createNestApplication();
20
21 // Add global pipes, filters, interceptors
22 app.useGlobalPipes(new ValidationPipe());
23 app.useGlobalFilters(new AllExceptionsFilter());
24 app.useGlobalInterceptors(new LoggingInterceptor());
25
26 await app.init();
27
28 httpService = app.get<HttpService>(HttpService);
29 });
30
31 afterEach(async () => {
32 await app.close();
33 });
34
35 describe('Tribute management flow', () => {
36 it('should handle full tribute management cycle', async () => {
37 // 1. Create a legionary (POST)
38 const createLegionaryResponse = await request(app.getHttpServer())
39 .post('/legionaries')
40 .send({
41 name: 'Longinus Argentus',
42 rank: 'Centurion',
43 skills: ['Navigation', 'Tribute Hunting']
44 })
45 .expect(201);
46
47 const legionariesId = createLegionaryResponse.body.id;
48
49 // 2. Add tribute (POST)
50 const createTributeResponse = await request(app.getHttpServer())
51 .post('/tributes')
52 .send({
53 name: 'Cursed Aztec Gold',
54 value: 10000,
55 location: 'Isla de Muerta',
56 discoveredBy: legionariesId
57 })
58 .expect(201);
59
60 const tributeId = createTributeResponse.body.id;
61
62 // 3. Get tribute list (GET)
63 const tributesResponse = await request(app.getHttpServer())
64 .get('/tributes')
65 .query({ discoveredBy: legionariesId })
66 .expect(200);
67
68 expect(tributesResponse.body.length).toBe(1);
69 expect(tributesResponse.body[0].name).toBe('Cursed Aztec Gold');
70
71 // 4. Update tribute (PUT)
72 await request(app.getHttpServer())
73 .put(`/tributes/${tributeId}`)
74 .send({
75 name: 'Cursed Aztec Gold',
76 value: 15000, // Increased value
77 location: 'Isla de Muerta',
78 condition: 'pristine'
79 })
80 .expect(200);
81
82 // 5. Check update (GET)
83 const updatedTributeResponse = await request(app.getHttpServer())
84 .get(`/tributes/${tributeId}`)
85 .expect(200);
86
87 expect(updatedTributeResponse.body.value).toBe(15000);
88
89 // 6. Delete tribute (DELETE)
90 await request(app.getHttpServer())
91 .delete(`/tributes/${tributeId}`)
92 .expect(200);
93
94 // 7. Check deletion (GET)
95 await request(app.getHttpServer())
96 .get(`/tributes/${tributeId}`)
97 .expect(404);
98 });
99
100 it('should handle validation errors', async () => {
101 // Invalid data
102 const response = await request(app.getHttpServer())
103 .post('/tributes')
104 .send({
105 name: '', // Empty name
106 value: -100, // Negative value
107 location: null // Null location
108 })
109 .expect(400);
110
111 expect(response.body.message).toContain('validation failed');
112 expect(response.body.errors).toHaveLength.toBeGreaterThan(0);
113 });
114
115 it('should handle authorization errors', async () => {
116 await request(app.getHttpServer())
117 .get('/tributes/secret')
118 .expect(401);
119
120 await request(app.getHttpServer())
121 .get('/tributes/secret')
122 .set('Authorization', 'Bearer invalid-token')
123 .expect(403);
124 });
125 });
126
127 describe('External API integration', () => {
128 it('should integrate with external weather API', async () => {
129 // Mock external API
130 const weatherApiResponse = {
131 condition: 'stormy',
132 windSpeed: 45,
133 waveHeight: 3.5,
134 visibility: 'poor'
135 };
136
137 jest.spyOn(httpService, 'get').mockImplementation(() =>
138 of({ data: weatherApiResponse } as AxiosResponse)
139 );
140
141 const response = await request(app.getHttpServer())
142 .get('/weather/check')
143 .query({ lat: 25.7617, lng: -80.1918 })
144 .expect(200);
145
146 expect(response.body.condition).toBe('stormy');
147 expect(response.body.marchingRecommendation).toBe('dangerous');
148 });
149 });
150});

4. WebSocket Integration Testing

1// websocket-integration.spec.ts
2describe('WebSocket Integration', () => {
3 let app: INestApplication;
4 let ws: Socket;
5
6 beforeEach(async () => {
7 const moduleFixture = await Test.createTestingModule({
8 imports: [EventsModule, AuthModule],
9 }).compile();
10
11 app = moduleFixture.createNestApplication();
12 await app.listen(3001);
13
14 // Connect to WebSocket
15 ws = io('http://localhost:3001', {
16 auth: {
17 token: 'valid-legionaries-token'
18 }
19 });
20
21 await new Promise(resolve => ws.on('connect', resolve));
22 });
23
24 afterEach(async () => {
25 ws.disconnect();
26 await app.close();
27 });
28
29 it('should handle real-time communication between legionaries', (done) => {
30 // Subscribe to events
31 ws.on('tribute-discovered', (data) => {
32 expect(data.tributeName).toBe('Golden Denarius');
33 expect(data.location).toBe('Hidden Cove');
34 expect(data.discoveredBy).toBe('Centurion Jack');
35 done();
36 });
37
38 // Emit event
39 ws.emit('discover-tribute', {
40 tributeName: 'Golden Denarius',
41 location: 'Hidden Cove',
42 discoveredBy: 'Centurion Jack'
43 });
44 });
45
46 it('should handle broadcasting to multiple clients', async () => {
47 // Create a second client
48 const ws2 = io('http://localhost:3001', {
49 auth: { token: 'another-legionaries-token' }
50 });
51
52 await new Promise(resolve => ws2.on('connect', resolve));
53
54 const messages = [];
55
56 ws.on('cohort-battle', (data) => messages.push({ client: 1, data }));
57 ws2.on('cohort-battle', (data) => messages.push({ client: 2, data }));
58
59 // Emit battle event
60 ws.emit('start-battle', {
61 attacker: 'Cohors Nigra',
62 defender: 'Cohors Barbarorum'
63 });
64
65 await new Promise(resolve => setTimeout(resolve, 100));
66
67 expect(messages).toHaveLength(2);
68 expect(messages[0].data.attacker).toBe('Cohors Nigra');
69 expect(messages[1].data.attacker).toBe('Cohors Nigra');
70
71 ws2.disconnect();
72 });
73});

Integration Testing Best Practices

1. Test Environment Isolation

1// test-config.ts
2export const testConfig = {
3 database: {
4 type: 'postgres',
5 host: process.env.TEST_DB_HOST || 'localhost',
6 port: parseInt(process.env.TEST_DB_PORT) || 5433, // Different port for tests
7 username: process.env.TEST_DB_USER || 'test_user',
8 password: process.env.TEST_DB_PASS || 'test_pass',
9 database: process.env.TEST_DB_NAME || 'legionaries_test_db',
10 synchronize: true,
11 dropSchema: true, // Clear database before each test
12 },
13 redis: {
14 host: process.env.TEST_REDIS_HOST || 'localhost',
15 port: parseInt(process.env.TEST_REDIS_PORT) || 6380, // Different port for tests
16 db: 1, // Different database for tests
17 },
18};

2. Data Setup and Cleanup

1// test-helpers.ts
2export class TestDataHelper {
3 constructor(private dataSource: DataSource) {}
4
5 async seedTestData() {
6 const cohorts = await this.dataSource.getRepository(Legion).save([
7 { name: 'Cohors Nigra', capacity: 50 },
8 { name: 'Ultio Reginae', capacity: 40 },
9 ]);
10
11 const legionaries = await this.dataSource.getRepository(Legionary).save([
12 { name: 'Caesar', rank: 'Centurion', cohortId: cohorts[0].id },
13 { name: 'Gaius Calidus', rank: 'Optio', cohortId: cohorts[0].id },
14 { name: 'Aurelia Bona', rank: 'Speculator', cohortId: cohorts[1].id },
15 ]);
16
17 return { cohorts, legionaries };
18 }
19
20 async cleanupTestData() {
21 await this.dataSource.getRepository(Tribute).delete({});
22 await this.dataSource.getRepository(Legionary).delete({});
23 await this.dataSource.getRepository(Legion).delete({});
24 }
25}

3. Parallel Test Execution

1// jest.config.js
2module.exports = {
3 // Run integration tests separately
4 projects: [
5 {
6 displayName: 'unit',
7 testMatch: ['<rootDir>/src/**/*.spec.ts'],
8 },
9 {
10 displayName: 'integration',
11 testMatch: ['<rootDir>/test/**/*.integration.spec.ts'],
12 maxWorkers: 2, // Limit parallelism for integration tests
13 },
14 ],
15};

Integration testing is checking whether different parts of your Roman fort work together like a well-coordinated legion. It's the key to finding problems that can only appear when components cooperate with each other!

Go to CodeWorlds