Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Test Coverage - mapowanie przetestowanych obszarów

Witaj, kartografie kodu! Konsul Caesar.js potrzebuje dokładnej mapy pokazującej, które obszary fortu zostały przebadane, a które wciąż czekają na inspekcję. Test Coverage to jak szczegółowa mapa przetestowanych fragmentów kodu!

Czym jest Test Coverage?

Test Coverage to metryka pokazująca, jaka część kodu jest wykonywana podczas testów. To jak mapa fortu pokazująca:

  • Przetestowane pokoje - linie kodu wykonane podczas testów
  • Niezbadane zakątki - kod, który nigdy nie jest wywoływany
  • Częściowo sprawdzone sekcje - funkcje testowane tylko w niektórych scenariuszach
  • Kompletnie zbadane obszary - kod w pełni pokryty testami
1# Uruchomienie testów z coverage w NestJS
2npm run test:cov
3
4# Wynik:
5# Statements : 87.5% ( 175/200 )
6# Branches : 83.3% ( 50/60 )
7# Functions : 90.0% ( 45/50 )
8# Lines : 88.0% ( 176/200 )

Rodzaje Coverage Metrics

1. Statement Coverage - pokrycie instrukcji

1// tribute-calculator.service.ts
2@Injectable()
3export class TributeCalculatorService {
4 calculateValue(tribute: Tribute): number {
5 let value = tribute.baseValue; // Linia 1 ✓
6 
7 if (tribute.rarity === 'legendary') { // Linia 2 ✓
8 value = value * 10; // Linia 3 - NIEPOKRYTA!
9 }
10 
11 if (tribute.condition === 'damaged') { // Linia 4 ✓
12 value = value * 0.5; // Linia 5 ✓
13 }
14 
15 return value; // Linia 6 ✓
16 }
17}
18
19// Test który nie pokrywa wszystkich linii
20describe('TributeCalculatorService', () => {
21 it('powinno obliczyć wartość uszkodzonego tributy', () => {
22 const tribute = {
23 baseValue: 1000,
24 rarity: 'common', // Nie testujemy 'legendary'!
25 condition: 'damaged'
26 };
27 
28 const result = service.calculateValue(tribute);
29 
30 expect(result).toBe(500);
31 });
32 
33 // Brakuje testu dla legendary tributes!
34});
35
36// Coverage: 83.3% (5/6 linii)

2. Branch Coverage - pokrycie rozgałęzień

1// navigation.service.ts
2@Injectable()
3export class NavigationService {
4 calculateRoute(weather: Weather, distance: number): RouteRecommendation {
5 // Branż 1: weather check
6 if (weather.condition === 'storm') { // TRUE branch ?
7 return { recommendation: 'stay_in_port', safety: 'dangerous' };
8 }
9 
10 // Branż 2: distance check 
11 if (distance > 1000) { // TRUE branch ✓, FALSE branch ?
12 return { recommendation: 'long_voyage', safety: 'moderate' };
13 }
14 
15 return { recommendation: 'short_trip', safety: 'safe' };
16 }
17}
18
19// Test nie pokrywający wszystkich branż
20describe('NavigationService', () => {
21 it('powinno zalecić krótki kampania', () => {
22 const result = service.calculateRoute(
23 { condition: 'calm' }, // storm=FALSE ✓
24 500 // distance<=1000, czyli FALSE ✓
25 );
26 
27 expect(result.recommendation).toBe('short_trip');
28 });
29 
30 // Brakuje testów dla:
31 // - weather.condition === 'storm' (TRUE)
32 // - distance > 1000 (TRUE)
33});
34
35// Branch Coverage: 50% (2/4 branches)

3. Function Coverage - pokrycie funkcji

1// legion.service.ts
2@Injectable()
3export class LegionService {
4 getAllLegionarys(): Legionary[] { // TESTOWANA ✓
5 return this.legionariuszes;
6 }
7 
8 addLegionary(legionariusze: Legionary): void { // TESTOWANA ✓
9 this.legionariuszes.push(legionariusze);
10 }
11 
12 promoteLegionary(id: string): void { // NIETESTOWANA!
13 const legionariusze = this.findById(id);
14 legionariusze.rank = this.getNextRank(legionariusze.rank);
15 }
16 
17 dismissLegionary(id: string): void { // NIETESTOWANA!
18 this.legionariuszes = this.legionariuszes.filter(p => p.id !== id);
19 }
20 
21 private findById(id: string): Legionary { // TESTOWANA pośrednio ✓
22 return this.legionariuszes.find(p => p.id === id);
23 }
24 
25 private getNextRank(currentRank: string): string { // NIETESTOWANA!
26 const ranks = ['Legionary', 'Ballistarius', 'Optio', 'Centurion'];
27 const currentIndex = ranks.indexOf(currentRank);
28 return ranks[currentIndex + 1] || currentRank;
29 }
30}
31
32// Function Coverage: 50% (3/6 funkcji)

Konfiguracja Coverage w NestJS

Jest Configuration

1// jest.config.js
2module.exports = {
3 moduleFileExtensions: ['js', 'json', 'ts'],
4 rootDir: 'src',
5 testRegex: '.*\.spec\.ts$',
6 transform: {
7 '^.+\.(t|j)s$': 'ts-jest',
8 },
9 collectCoverageFrom: [
10 '**/*.(t|j)s',
11 '!**/*.spec.ts',
12 '!**/*.e2e-spec.ts',
13 '!**/node_modules/**',
14 '!**/dist/**',
15 '!**/*.interface.ts',
16 '!**/*.dto.ts',
17 '!**/main.ts',
18 ],
19 coverageDirectory: '../coverage',
20 testEnvironment: 'node',
21 
22 // Coverage thresholds - minimalne wymagania
23 coverageThreshold: {
24 global: {
25 branches: 80,
26 functions: 80,
27 lines: 80,
28 statements: 80
29 },
30 // Specjalne wymagania dla ważnych plików
31 './src/tribute/*.ts': {
32 branches: 90,
33 functions: 95,
34 lines: 90,
35 statements: 90
36 },
37 './src/security/*.ts': {
38 branches: 95,
39 functions: 100,
40 lines: 95,
41 statements: 95
42 }
43 },
44 
45 // Raporty coverage
46 coverageReporters: ['text', 'lcov', 'html', 'json'],
47};

Package.json Scripts

1{
2 "scripts": {
3 "test": "jest",
4 "test:watch": "jest --watch",
5 "test:cov": "jest --coverage",
6 "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
7 "test:cov:watch": "jest --coverage --watch",
8 "test:cov:html": "jest --coverage && open coverage/lcov-report/index.html"
9 }
10}

Strategia poprawy Coverage

1. Identyfikacja brakujących testów

1// Analiza coverage report
2// File: tribute.service.ts
3// Uncovered Lines: 23, 45-48, 67
4
5// Linia 23: Error handling
6if (!tribute) {
7 throw new NotFoundException('Tribute not found'); // Nie testowana!
8}
9
10// Linie 45-48: Edge case
11if (tribute.value < 0) { // Nie testowana!
12 tribute.value = 0;
13 this.logger.warn('Negative tribute value corrected');
14}
15
16// Linia 67: Async error
17catch (error) {
18 this.logger.error('Database error', error); // Nie testowana!
19 throw new InternalServerErrorException();
20}
21
22// Dodaj brakujące testy:
23it('powinno rzucić wyjątek gdy tributy nie istnieje', () => {
24 mockRepository.findOne.mockResolvedValue(null);
25 
26 expect(() => service.findById('non-existent'))
27 .rejects.toThrow(NotFoundException);
28});
29
30it('powinno skorygować ujemną wartość tributy', () => {
31 const tribute = { id: 1, name: 'Test', value: -100 };
32 
33 const result = service.validateTribute(tribute);
34 
35 expect(result.value).toBe(0);
36 expect(mockLogger.warn).toHaveBeenCalledWith('Negative tribute value corrected');
37});
38
39it('powinno obsłużyć błąd bazy danych', async () => {
40 mockRepository.save.mockRejectedValue(new Error('DB Error'));
41 
42 await expect(service.saveTribute(tribute))
43 .rejects.toThrow(InternalServerErrorException);
44 
45 expect(mockLogger.error).toHaveBeenCalledWith('Database error', expect.any(Error));
46});

2. Testowanie Edge Cases

1// Kompletne pokrycie funkcji walidacji
2describe('Legionary Validation', () => {
3 describe('validateLegionaryRank', () => {
4 // Happy path
5 it('powinno zaakceptować poprawną rangę', () => {
6 expect(validator.validateLegionaryRank('Centurion')).toBe(true);
7 });
8 
9 // Edge cases dla kompletnego coverage
10 it('powinno odrzucić null', () => {
11 expect(validator.validateLegionaryRank(null)).toBe(false);
12 });
13 
14 it('powinno odrzucić undefined', () => {
15 expect(validator.validateLegionaryRank(undefined)).toBe(false);
16 });
17 
18 it('powinno odrzucić pusty string', () => {
19 expect(validator.validateLegionaryRank('')).toBe(false);
20 });
21 
22 it('powinno odrzucić niewiadomy rangę', () => {
23 expect(validator.validateLegionaryRank('Legate')).toBe(false);
24 });
25 
26 it('powinno być case insensitive', () => {
27 expect(validator.validateLegionaryRank('centurion')).toBe(true);
28 expect(validator.validateLegionaryRank('CENTURION')).toBe(true);
29 });
30 
31 it('powinno obsłużyć spacje', () => {
32 expect(validator.validateLegionaryRank(' Centurion ')).toBe(true);
33 });
34 });
35});

3. Parametrized Tests dla wysokiego coverage

1// Użycie test.each dla różnych scenariuszy
2describe('Battle Damage Calculator', () => {
3 // Test wszystkich kombinacji broni i pancerza
4 test.each([
5 ['ballista', 'wood', 50, 45],
6 ['ballista', 'iron', 50, 35],
7 ['ballista', 'steel', 50, 25],
8 ['pilum', 'wood', 25, 22],
9 ['pilum', 'iron', 25, 15],
10 ['pilum', 'steel', 25, 8],
11 ['gladius', 'wood', 15, 12],
12 ['gladius', 'iron', 15, 8],
13 ['gladius', 'steel', 15, 4],
14 ])('%s vs %s armor should deal %i base damage, resulting in %i actual damage', 
15 (weapon, armor, baseDamage, expectedDamage) => {
16 const result = calculator.calculateDamage(weapon, armor, baseDamage);
17 expect(result).toBe(expectedDamage);
18 }
19 );
20 
21 // Test wszystkich kombinacji pogody
22 test.each([
23 ['calm', 1.0],
24 ['windy', 0.9],
25 ['stormy', 0.7],
26 ['fog', 0.8],
27 ['rain', 0.85],
28 ])('weather %s should have multiplier %f', (weather, multiplier) => {
29 const result = calculator.getWeatherMultiplier(weather);
30 expect(result).toBeCloseTo(multiplier);
31 });
32});

Coverage Analysis i Continuous Integration

1. GitHub Actions z Coverage

1# .github/workflows/test.yml
2name: Test and Coverage
3
4on: [push, pull_request]
5
6jobs:
7 test:
8 runs-on: ubuntu-latest
9 
10 steps:
11 - uses: actions/checkout@v3
12 
13 - name: Setup Node.js
14 uses: actions/setup-node@v3
15 with:
16 node-version: '18'
17 cache: 'npm'
18 
19 - name: Install dependencies
20 run: npm ci
21 
22 - name: Run tests with coverage
23 run: npm run test:cov
24 
25 - name: Upload coverage to Codecov
26 uses: codecov/codecov-action@v3
27 with:
28 file: ./coverage/lcov.info
29 flags: unittests
30 
31 - name: Coverage Comment
32 uses: romeovs/lcov-reporter-action@v0.3.1
33 with:
34 github-token: ${{ secrets.GITHUB_TOKEN }}
35 lcov-file: ./coverage/lcov.info

2. Pre-commit Hook

1// package.json
2{
3 "husky": {
4 "hooks": {
5 "pre-commit": "lint-staged"
6 }
7 },
8 "lint-staged": {
9 "*.{ts,js}": [
10 "eslint --fix",
11 "npm run test:cov -- --passWithNoTests --findRelatedTests"
12 ]
13 }
14}

3. Coverage Badge

1<!-- README.md -->
2# Legionary Legion Backend
3
4![Coverage Badge](https://img.shields.io/codecov/c/github/username/legionariusze-cohort-backend)
5![Tests](https://github.com/username/legionariusze-cohort-backend/workflows/Test%20and%20Coverage/badge.svg)
6
7## Test Coverage Report
8
9| Type | Percentage |
10|------|------------|
11| Statements | 87.5% |
12| Branches | 83.3% |
13| Functions | 90.0% |
14| Lines | 88.0% |

Coverage nie znaczy jakość!

Anti-patterns Coverage

1// ❌ Zły przykład - testowanie dla pokrycia, nie jakości
2it('should call all methods for coverage', () => {
3 service.method1(); // Wywołaj wszystko...
4 service.method2(); // ...bez sprawdzania wyników
5 service.method3(); // 100% coverage, 0% wartości!
6 
7 expect(true).toBe(true); // Bezwartościowy test
8});
9
10// ✅ Dobry przykład - meaningful tests
11it('should calculate tribute value correctly', () => {
12 const tribute = { base: 100, rarity: 2.5, condition: 0.8 };
13 
14 const result = service.calculateValue(tribute);
15 
16 expect(result).toBe(200); // base * rarity * condition
17 expect(result).toBeGreaterThan(tribute.base);
18});

Metrics Balance

1// Dobra balance metryk:
2// - 80-90% Statement Coverage (ogólne pokrycie)
3// - 75-85% Branch Coverage (wszystkie ścieżki)
4// - 85-95% Function Coverage (wszystkie funkcje)
5// - Wysoka jakość testów (assertion quality)
6
7// Cele dla różnych typów kodu:
8const coverageTargets = {
9 businessLogic: { statements: 90, branches: 85, functions: 95 },
10 controllers: { statements: 80, branches: 75, functions: 90 },
11 utilities: { statements: 95, branches: 90, functions: 100 },
12 configurations: { statements: 70, branches: 60, functions: 80 }
13};

Test Coverage to kompas pokazujący, które obszary fortu są dobrze zbadane, a które wymagają więcej uwagi. Pamiętaj - nie chodzi o 100% pokrycie, ale o pokrycie tego, co naprawdę ma znaczenie dla bezpieczeństwa wyprawy!

Przejdź do CodeWorlds