Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Integration Testing - sprawdzanie współpracy legionu

Witaj, koordynatorze kampanii! Konsul Caesar.js zauważył, że choć każdy element fortu działa idealnie z osobna, czasami gdy muszą współpracować, pojawiają się niespodziewane problemy. Czas nauczyć się integration testing - sprawdzania czy różne części legionu potrafią działać razem jak dobrze naoliwiona maszyna!

Czym jest Integration Testing?

Wyobraź sobie, że twój fort to orkiestra. Każdy muzyk (unit) gra perfekcyjnie swoją partię, ale czy razem stworzą piękną melodię, czy kakofonię? Integration testing sprawdza:

  • Czy zwiadowca współpracuje z centurionem w planowaniu trasy
  • Czy artylerzyści koordynują ogień z rozkazami dowódcy
  • Czy system komunikacji sprawnie przekazuje informacje między oddziałami
  • Czy baza danych prawidłowo zapisuje wyniki operacji
1// Przykład problemu który wykrywa integration testing
2describe('Tribute Hunt Coordination', () => {
3 it('powinno skoordynować całą wyprawę po tributy', async () => {
4 // Te komponenty muszą współpracować
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});

Strategie Integration Testing w NestJS

1. Module Integration Testing

Testowanie jak różne moduły współpracują ze sobą:

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('powinno przeprowadzić pełną wyprawę od planowania do podziału tributów', async () => {
34 // 1. Przygotuj legion
35 const centurion = await legionService.recruitLegionary({
36 name: 'Centurion Hook',
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. Zaplanuj wyprawę
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. Przeprowadź wyprawę
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. Sprawdź czy tributy zostały dodane do bazy
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. Sprawdź czy legion otrzymała doświadczenie
71 const updatedLegion = await Promise.all(
72 legion.map(p => legionService.findById(p.id))
73 );
74
75 updatedLegion.forEach(legionariusze => {
76 expect(legionariusze.experience).toBeGreaterThan(legion.find(c => c.id === legionariusze.id).experience);
77 });
78
79 // 6. Sprawdź podział tributów
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('powinno obsłużyć niepowodzenie wyprawy', async () => {
89 const centurion = await legionService.recruitLegionary({
90 name: 'Inexperienced Centurion',
91 rank: 'Centurion',
92 skills: [], // Brak umiejętności
93 experience: 0
94 });
95
96 const expeditionPlan = await expeditionService.planExpedition({
97 centurionId: centurion.id,
98 legionIds: [],
99 destination: { lat: 0, lng: 0 }, // Niebezpieczne miejsce
100 estimatedDuration: 30 // Zbyt długo
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

Testowanie współpracy z rzeczywistą bazą danych:

1// database-integration.spec.ts
2describe('Database Integration', () => {
3 let app: TestingModule;
4 let dataSource: DataSource;
5 let tributeRepository: Repository<Tribute>;
6 let legionariuszeRepository: 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 || 'legionariusze_test',
18 entities: [Tribute, Legionary, Legion],
19 synchronize: true,
20 dropSchema: true, // Czyści bazę przed każdym testem
21 }),
22 TributeModule,
23 LegionModule,
24 ],
25 }).compile();
26
27 dataSource = app.get<DataSource>(DataSource);
28 tributeRepository = dataSource.getRepository(Tribute);
29 legionariuszeRepository = dataSource.getRepository(Legionary);
30 });
31
32 afterEach(async () => {
33 await app.close();
34 });
35
36 describe('Transaction handling', () => {
37 it('powinno wykonać transakcję transferu tributy', async () => {
38 const queryRunner = dataSource.createQueryRunner();
39 await queryRunner.connect();
40 await queryRunner.startTransaction();
41
42 try {
43 // Stwórz legionariuszów
44 const legionariusze1 = await queryRunner.manager.save(Legionary, {
45 name: 'Marcus Aquila',
46 rank: 'Centurion',
47 gold: 1000
48 });
49
50 const legionariusze2 = await queryRunner.manager.save(Legionary, {
51 name: 'Will Turner',
52 rank: 'Legionary',
53 gold: 100
54 });
55
56 // Stwórz tributy
57 const tribute = await queryRunner.manager.save(Tribute, {
58 name: 'Golden Compass',
59 value: 500,
60 ownerId: legionariusze1.id
61 });
62
63 // Transfer tributy
64 await queryRunner.manager.update(Tribute, tribute.id, {
65 ownerId: legionariusze2.id
66 });
67
68 await queryRunner.manager.update(Legionary, legionariusze1.id, {
69 gold: legionariusze1.gold + tribute.value
70 });
71
72 await queryRunner.manager.update(Legionary, legionariusze2.id, {
73 gold: legionariusze2.gold - tribute.value
74 });
75
76 await queryRunner.commitTransaction();
77
78 // Sprawdź wyniki
79 const updatedTribute = await tributeRepository.findOne({
80 where: { id: tribute.id }
81 });
82 const updatedLegionary1 = await legionariuszeRepository.findOne({
83 where: { id: legionariusze1.id }
84 });
85 const updatedLegionary2 = await legionariuszeRepository.findOne({
86 where: { id: legionariusze2.id }
87 });
88
89 expect(updatedTribute.ownerId).toBe(legionariusze2.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('powinno wycofać transakcję przy błędzie', async () => {
102 const queryRunner = dataSource.createQueryRunner();
103 await queryRunner.connect();
104 await queryRunner.startTransaction();
105
106 try {
107 const legionariusze = await queryRunner.manager.save(Legionary, {
108 name: 'Test Legionary',
109 rank: 'Legionary',
110 gold: 100
111 });
112
113 // Symulacja błędu
114 await queryRunner.manager.query('INVALID SQL QUERY');
115 
116 await queryRunner.commitTransaction();
117 } catch (error) {
118 await queryRunner.rollbackTransaction();
119 
120 // Sprawdź czy legionariusz nie został zapisany
121 const legionariuszes = await legionariuszeRepository.find({ where: { name: 'Test Legionary' } });
122 expect(legionariuszes.length).toBe(0);
123 } finally {
124 await queryRunner.release();
125 }
126 });
127 });
128
129 describe('Complex queries', () => {
130 it('powinno wykonać złożone zapytanie z JOIN', async () => {
131 // Stwórz dane testowe
132 const cohort = await dataSource.getRepository(Legion).save({
133 name: 'Equus Niger',
134 capacity: 50
135 });
136
137 const centurion = await legionariuszeRepository.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 // Wykonaj złożone zapytanie
150 const result = await dataSource
151 .getRepository(Legionary)
152 .createQueryBuilder('legionariusze')
153 .leftJoinAndSelect('legionariusze.cohort', 'cohort')
154 .leftJoin('legionariusze.tributes', 'tribute')
155 .select([
156 'legionariusze.name as userName',
157 'legionariusze.rank as rank',
158 'cohort.name as cohortName',
159 'COUNT(tribute.id) as tributeCount',
160 'SUM(tribute.value) as totalValue'
161 ])
162 .where('legionariusze.rank = :rank', { rank: 'Centurion' })
163 .groupBy('legionariusze.id, cohort.id')
164 .getRawOne();
165
166 expect(result.userName).toBe('Caesar');
167 expect(result.cohortName).toBe('Equus Niger');
168 expect(parseInt(result.tributeCount)).toBe(3);
169 expect(parseInt(result.totalValue)).toBe(450);
170 });
171 });
172});

3. HTTP Integration Testing

Testowanie pełnego flow HTTP requests:

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 // Dodaj globalne 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('powinno obsłużyć pełny cykl zarządzania tributami', async () => {
37 // 1. Utwórz legionariusza (POST)
38 const createLegionaryResponse = await request(app.getHttpServer())
39 .post('/legionariuszes')
40 .send({
41 name: 'Longinus Argentus',
42 rank: 'Centurion',
43 skills: ['Navigation', 'Tribute Hunting']
44 })
45 .expect(201);
46
47 const legionariuszeId = createLegionaryResponse.body.id;
48
49 // 2. Dodaj tributy (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: legionariuszeId
57 })
58 .expect(201);
59
60 const tributeId = createTributeResponse.body.id;
61
62 // 3. Pobierz listę tributów (GET)
63 const tributesResponse = await request(app.getHttpServer())
64 .get('/tributes')
65 .query({ discoveredBy: legionariuszeId })
66 .expect(200);
67
68 expect(tributesResponse.body.length).toBe(1);
69 expect(tributesResponse.body[0].name).toBe('Cursed Aztec Gold');
70
71 // 4. Zaktualizuj tributy (PUT)
72 await request(app.getHttpServer())
73 .put(`/tributes/${tributeId}`)
74 .send({
75 name: 'Cursed Aztec Gold',
76 value: 15000, // Zwiększona wartość
77 location: 'Isla de Muerta',
78 condition: 'pristine'
79 })
80 .expect(200);
81
82 // 5. Sprawdź aktualizację (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. Usuń tributy (DELETE)
90 await request(app.getHttpServer())
91 .delete(`/tributes/${tributeId}`)
92 .expect(200);
93
94 // 7. Sprawdź usunięcie (GET)
95 await request(app.getHttpServer())
96 .get(`/tributes/${tributeId}`)
97 .expect(404);
98 });
99
100 it('powinno obsłużyć błędy walidacji', async () => {
101 // Nieprawidłowe dane
102 const response = await request(app.getHttpServer())
103 .post('/tributes')
104 .send({
105 name: '', // Pusta nazwa
106 value: -100, // Ujemna wartość
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('powinno obsłużyć błędy autoryzacji', 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('powinno integrować się z zewnętrznym API pogodowym', 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 // Połącz z WebSocket
15 ws = io('http://localhost:3001', {
16 auth: {
17 token: 'valid-legionariusze-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('powinno obsłużyć komunikację real-time między legionariuszami', (done) => {
30 // Subskrybuj na eventy
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 // Wyślij event
39 ws.emit('discover-tribute', {
40 tributeName: 'Golden Denarius',
41 location: 'Hidden Cove',
42 discoveredBy: 'Centurion Jack'
43 });
44 });
45
46 it('powinno obsłużyć broadcasting do wielu klientów', async () => {
47 // Stwórz drugiego klienta
48 const ws2 = io('http://localhost:3001', {
49 auth: { token: 'another-legionariusze-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 // Wyślij battle event
60 ws.emit('start-battle', {
61 attacker: 'Equus Niger',
62 defender: 'Batavus Volans'
63 });
64
65 await new Promise(resolve => setTimeout(resolve, 100));
66
67 expect(messages).toHaveLength(2);
68 expect(messages[0].data.attacker).toBe('Equus Niger');
69 expect(messages[1].data.attacker).toBe('Equus Niger');
70
71 ws2.disconnect();
72 });
73});

Najlepsze praktyki Integration Testing

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, // Inny port dla testów
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 || 'legionariusze_test_db',
10 synchronize: true,
11 dropSchema: true, // Czyść bazę przed każdym testem
12 },
13 redis: {
14 host: process.env.TEST_REDIS_HOST || 'localhost',
15 port: parseInt(process.env.TEST_REDIS_PORT) || 6380, // Inny port dla testów
16 db: 1, // Inna baza dla testów
17 },
18};

2. Data Setup i 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: 'Equus Niger', capacity: 50 },
8 { name: 'Ultio Reginae', capacity: 40 },
9 ]);
10
11 const legionariuszes = 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, legionariuszes };
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 // Uruchom testy integration osobno
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, // Ogranicz równoległość dla testów integration
13 },
14 ],
15};

Integration testing to sprawdzanie czy różne części Twojego rzymskiego fortu działają razem jak dobrze zgrany legion. To klucz do odnalezienia problemów, które mogą pojawić się tylko gdy komponenty współpracują ze sobą!

Ir a CodeWorlds