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!
Test Coverage to metryka pokazująca, jaka część kodu jest wykonywana podczas testów. To jak mapa fortu pokazująca:
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 )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)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)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)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};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}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});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});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});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.info1// 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}1<!-- README.md -->
2# Legionary Legion Backend
3
4
5
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% |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});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!