Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Testing - próby przed wyruszeniem na kampanię

Witaj, testerze legionów! Konsul Caesar.js wie dobrze, że żaden mądry legionista nie wyrusza na kampanię bez dokładnego sprawdzenia każdego elementu fortu. Dziś nauczysz się sztuki testowania - najważniejszej umiejętności każdego profesjonalnego legionisty-programisty!

W rzymskim forcie nie ma miejsca na błędy. Jeśli katapulta nie wystrzeli w kluczowym momencie, jeśli mury nie wytrzymają oblężenia, lub jeśli system obronny zawiedzie w trakcie ataku - to oznacza katastrofę! Dlatego każdy element musi być przetestowany zanim wyruszymy na kampanię.

Dlaczego testowanie jest krytyczne?

Wyobraź sobie, że jesteś centurionem odpowiedzialnym za bezpieczeństwo całego legionu. Zanim wyruszysz na kampanię, musisz sprawdzić:

  • Czy machiny wojenne działają - unit testy
  • Czy legion współpracuje - integration testy
  • Czy cały fort jest gotowy do kampanii - end-to-end testy
  • Jak fort radzi sobie podczas oblężenia - stress testy
  • Czy wszystkie systemy działają niezawodnie - reliability testy
1// Podstawowa filozofia testów w rzymskim forcie
2describe('aplikacja', () => {
3 it('powinien być gotowy do wymarszu', () => {
4 const cohort = new Legionary();
5 expect(cohort.isCombatReady()).toBe(true);
6 });
7
8 it('powinien przetrwać burzę', () => {
9 const cohort = new Legionary();
10 const storm = new Storm({ intensity: 'high' });
11 
12 expect(() => cohort.weatherStorm(storm)).not.toThrow();
13 expect(cohort.status).toBe('operational');
14 });
15});

Rodzaje testów w rzymskim forcie

1. Unit Tests - testowanie pojedynczych komponentów

To jak sprawdzanie każdej machiny wojennej z osobna - czy ładuje się prawidłowo, czy strzela w odpowiednim kierunku, czy nie eksploduje:

1// ballista.spec.ts
2describe('Ballista', () => {
3 let ballista: Ballista;
4
5 beforeEach(() => {
6 ballista = new Ballista('Front Line Ballista #1');
7 });
8
9 it('powinno się załadować amunicją', () => {
10 ballista.load(BoltType.IRON);
11 expect(ballista.isLoaded()).toBe(true);
12 expect(ballista.getAmmunitionType()).toBe(BoltType.IRON);
13 });
14
15 it('powinno oddać strzał gdy jest załadowana', () => {
16 ballista.load(BoltType.IRON);
17 const result = ballista.fire();
18 
19 expect(result.hit).toBeDefined();
20 expect(ballista.isLoaded()).toBe(false);
21 });
22
23 it('nie powinno strzelać gdy nie jest załadowana', () => {
24 expect(() => ballista.fire()).toThrow('Ballista not loaded!');
25 });
26});

2. Integration Tests - testowanie współpracy

To jak ćwiczenia bojowe całego legionu - czy machiny wojenne współpracują z systemem dowodzenia, czy komunikacja między oddziałami działa:

1// cohort-combat.integration.spec.ts
2describe('Legion Combat Integration', () => {
3 let cohort: Legionary;
4 let enemy: EnemyLegion;
5
6 beforeEach(() => {
7 cohort = new Legionary();
8 cohort.addBallista(new Ballista('Left Flank #1'));
9 cohort.addBallista(new Ballista('Right Flank #1'));
10 enemy = new EnemyLegion();
11 });
12
13 it('powinno koordynować atak wszystkich balist', async () => {
14 const battleResult = await cohort.attackEnemy(enemy);
15 
16 expect(battleResult.shotsfired).toBe(2);
17 expect(battleResult.hits).toBeGreaterThan(0);
18 expect(enemy.health).toBeLessThan(enemy.maxHealth);
19 });
20});

3. End-to-End Tests - pełna symulacja kampanii

To jak kompletny test wyprawy - od wymarszu z kastrum, przez walkę z barbarzyńcami, po powrót z tributami:

1// tribute-hunt.e2e.spec.ts
2describe('Complete Tribute Hunt', () => {
3 let app: INestApplication;
4
5 beforeEach(async () => {
6 const moduleFixture = await Test.createTestingModule({
7 imports: [AppModule],
8 }).compile();
9
10 app = moduleFixture.createNestApplication();
11 await app.init();
12 });
13
14 it('powinno przeprowadzić kompletną wyprawę po tributy', async () => {
15 // 1. Zaplanuj wyprawę
16 const expeditionPlan = await request(app.getHttpServer())
17 .post('/expeditions/plan')
18 .send({
19 destination: 'Tribute Island',
20 legionSize: 20,
21 duration: 7
22 })
23 .expect(201);
24
25 // 2. Przeprowadź wyprawę
26 const expedition = await request(app.getHttpServer())
27 .post('/expeditions/execute')
28 .send({ planId: expeditionPlan.body.id })
29 .expect(201);
30
31 // 3. Sprawdź wyniki
32 expect(expedition.body.success).toBe(true);
33 expect(expedition.body.tributesFound).toBeGreaterThan(0);
34 expect(expedition.body.legionStatus).toBe('healthy');
35 });
36});

Framework testowy w NestJS

NestJS domyślnie używa Jest - potężnego frameworka testowego, który pozwala na wszystkie rodzaje testów:

1// package.json
2{
3 "scripts": {
4 "test": "jest",
5 "test:watch": "jest --watch",
6 "test:cov": "jest --coverage",
7 "test:e2e": "jest --config ./test/jest-e2e.json"
8 },
9 "jest": {
10 "moduleFileExtensions": ["js", "json", "ts"],
11 "rootDir": "src",
12 "testRegex": ".*\.spec\.ts$",
13 "transform": {
14 "^.+\.(t|j)s$": "ts-jest"
15 },
16 "collectCoverageFrom": [
17 "**/*.(t|j)s"
18 ],
19 "coverageDirectory": "../coverage",
20 "testEnvironment": "node"
21 }
22}

Podstawowe koncepcje testowe

Test Doubles - dublery w aplikacji

Czasami potrzebujemy zastąpić prawdziwe komponenty ich imitacjami:

1// Mock - imitacja zewnętrznych serwisów
2const mockWeatherService = {
3 getWeather: jest.fn().mockResolvedValue({
4 condition: 'sunny',
5 windSpeed: 15,
6 temperature: 25
7 })
8};
9
10// Spy - szpiegowanie wywołań metod
11const spyOnBallistaFire = jest.spyOn(ballista, 'fire');
12
13// Stub - ustalone odpowiedzi
14const tributeServiceStub = {
15 findTribute: () => Promise.resolve(new Tribute('Golden Coin'))
16};

Test Lifecycle - cykl życia testów

1describe('Legionary Legion Management', () => {
2 let legion: Legionary;
3
4 // Przed wszystkimi testami
5 beforeAll(async () => {
6 // Konfiguracja globalna
7 await setupTestDatabase();
8 });
9
10 // Przed każdym testem
11 beforeEach(() => {
12 legion = new Legionary();
13 legion.addLegionary(new Legionary('Marcus Aquila', 'Centurion'));
14 });
15
16 // Po każdym teście
17 afterEach(() => {
18 legion.dismiss();
19 });
20
21 // Po wszystkich testach
22 afterAll(async () => {
23 await cleanupTestDatabase();
24 });
25
26 it('powinno zarządzać legionem', () => {
27 expect(legion.size()).toBe(1);
28 expect(legion.getCenturion().name).toBe('Marcus Aquila');
29 });
30});

Testowanie w NestJS - specjalne narzędzia

Test Module Builder

1// legion.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3
4describe('LegionService', () => {
5 let service: LegionService;
6 let module: TestingModule;
7
8 beforeEach(async () => {
9 module = await Test.createTestingModule({
10 providers: [
11 LegionService,
12 {
13 provide: 'LegionaryRepository',
14 useValue: mockLegionaryRepository,
15 },
16 ],
17 }).compile();
18
19 service = module.get<LegionService>(LegionService);
20 });
21
22 afterEach(async () => {
23 await module.close();
24 });
25
26 it('powinno rekrutować nowego legionariusza', async () => {
27 const legionariuszeData = { name: 'Aurelia Bona', rank: 'Speculator' };
28 const result = await service.recruitLegionary(legionariuszeData);
29 
30 expect(result.name).toBe('Aurelia Bona');
31 expect(mockLegionaryRepository.save).toHaveBeenCalledWith(legionariuszeData);
32 });
33});

Najlepsze praktyki testowania

1. AAA Pattern - Arrange, Act, Assert

1it('powinno obliczyć wartość tributy z podatkiem', () => {
2 // Arrange - przygotowanie
3 const tribute = new Tribute('Golden Chalice', 1000);
4 const taxCalculator = new TaxCalculator();
5 
6 // Act - wykonanie
7 const totalValue = taxCalculator.calculateWithTax(tribute, 0.1);
8 
9 // Assert - sprawdzenie
10 expect(totalValue).toBe(1100);
11});

2. Test Isolation - izolacja testów

1// ✅ Dobry test - niezależny
2it('powinno znaleźć tributy po ID', () => {
3 const tribute = tributeService.findById(1);
4 expect(tribute.name).toBe('Golden Compass');
5});
6
7// ❌ Zły test - zależny od innych testów
8let globalTributeId;
9it('powinno dodać tributy', () => {
10 globalTributeId = tributeService.add(new Tribute('Ruby'));
11});
12it('powinno znaleźć dodany tributy', () => {
13 const tribute = tributeService.findById(globalTributeId);
14 expect(tribute).toBeDefined();
15});

3. Descriptive Test Names - opisowe nazwy

1// ✅ Dobra nazwa
2it('powinno rzucić wyjątek gdy legionariusz próbuje podnieść tributy cięższy niż jego maksymalna waga', () => {
3 // test
4});
5
6// ❌ Słaba nazwa 
7it('test podnoszenia', () => {
8 // test
9});

Przykład kompletnego testu serwisu

1// tribute.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { TributeService } from './tribute.service';
5import { Tribute } from './tribute.entity';
6import { Repository } from 'typeorm';
7
8describe('TributeService', () => {
9 let service: TributeService;
10 let repository: Repository<Tribute>;
11
12 const mockRepository = {
13 find: jest.fn(),
14 findOne: jest.fn(),
15 save: jest.fn(),
16 delete: jest.fn(),
17 create: jest.fn(),
18 };
19
20 beforeEach(async () => {
21 const module: TestingModule = await Test.createTestingModule({
22 providers: [
23 TributeService,
24 {
25 provide: getRepositoryToken(Tribute),
26 useValue: mockRepository,
27 },
28 ],
29 }).compile();
30
31 service = module.get<TributeService>(TributeService);
32 repository = module.get<Repository<Tribute>>(getRepositoryToken(Tribute));
33 });
34
35 afterEach(() => {
36 jest.clearAllMocks();
37 });
38
39 describe('findAll', () => {
40 it('powinno zwrócić tablicę tributów', async () => {
41 const tributes = [
42 { id: 1, name: 'Golden Coin', value: 100 },
43 { id: 2, name: 'Silver Ring', value: 50 },
44 ];
45 
46 mockRepository.find.mockResolvedValue(tributes);
47
48 const result = await service.findAll();
49
50 expect(result).toEqual(tributes);
51 expect(repository.find).toHaveBeenCalledTimes(1);
52 });
53
54 it('powinno zwrócić pustą tablicę gdy brak tributów', async () => {
55 mockRepository.find.mockResolvedValue([]);
56
57 const result = await service.findAll();
58
59 expect(result).toEqual([]);
60 expect(result.length).toBe(0);
61 });
62 });
63
64 describe('create', () => {
65 it('powinno utworzyć nowy tributy', async () => {
66 const tributeData = { name: 'Diamond Ring', value: 5000 };
67 const expectedTribute = { id: 1, ...tributeData };
68
69 mockRepository.create.mockReturnValue(expectedTribute);
70 mockRepository.save.mockResolvedValue(expectedTribute);
71
72 const result = await service.create(tributeData);
73
74 expect(result).toEqual(expectedTribute);
75 expect(repository.create).toHaveBeenCalledWith(tributeData);
76 expect(repository.save).toHaveBeenCalledWith(expectedTribute);
77 });
78
79 it('powinno rzucić wyjątek dla nieprawidłowych danych', async () => {
80 const invalidData = { name: '', value: -100 };
81 mockRepository.save.mockRejectedValue(new Error('Validation failed'));
82
83 await expect(service.create(invalidData)).rejects.toThrow('Validation failed');
84 });
85 });
86});

Testowanie to nie tylko sprawdzanie, czy kod działa - to gwarancja, że Twoja aplikacja przetrwa każde oblężenie i dotrze do celu z całym legionem i tributami! W następnych modułach poznamy szczegóły różnych rodzajów testów.

Vai a CodeWorlds