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ę.
Wyobraź sobie, że jesteś centurionem odpowiedzialnym za bezpieczeństwo całego legionu. Zanim wyruszysz na kampanię, musisz sprawdzić:
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});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});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});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});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}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};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});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});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});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});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});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.