Kurs NestJS · Moduł 7: Testowanie
Test Coverage - mapowanie przetestowanych obszarów
W tej lekcji4
Napisałeś już testy jednostkowe, integracyjne i E2E. Ale skąd wiesz, że nie zostawiłeś w kodzie ciemnego zaułka, do którego żaden test nigdy nie zajrzał?
Kartograf legionu ma na to sposób. Bierze mapę Imperium i zaznacza na niej każdą drogę, którą przeszedł zwiadowca. To, co zostanie białą plamą, nikt nie sprawdził. Test coverage - pokrycie kodu testami - jest właśnie taką mapą: uruchamiasz testy, a narzędzie zapisuje, które linie kodu naprawdę się wykonały.
Odczytanie mapy
Raport zamawiasz jednym poleceniem:
1npm run test:covTen skrypt to nic innego jak jest --coverage - Jest wykonuje wszystkie testy, obserwując po drodze, których fragmentów kodu dotknął. Wynik wygląda tak:
1Statements : 87.5% ( 175/200 )
2Branches : 83.3% ( 50/60 )
3Functions : 90.0% ( 45/50 )
4Lines : 88.0% ( 176/200 )Cztery liczby, cztery różne pytania o ten sam kod. Warto je rozdzielić, bo różnią się dokładnością.
Statements i Lines liczą wykonane instrukcje i linie - to najgrubszy pomiar, mówi tylko "tędy przechodziliśmy". Functions pyta, czy każda funkcja została choć raz wywołana; przydatne do wyłapania metod, o których wszyscy zapomnieli.
Najważniejsza jest trzecia liczba. Branches mierzy ścieżki warunkowe - czy każde if sprawdzono zarówno wtedy, gdy warunek był prawdziwy, jak i wtedy, gdy fałszywy. Spójrz na tę funkcję:
1function calculatePay(legionary: Legionary): number {
2 if (legionary.rank === 'CENTURION') {
3 return legionary.basePay * 2;
4 }
5 return legionary.basePay;
6}Jeden test z centurionem wykona obie linijki poza return legionary.basePay - statement coverage podskoczy wysoko. Ale gałąź "to nie centurion" pozostanie niesprawdzona, a to właśnie w niej lubią siedzieć błędy. Dlatego branch coverage jest zawsze niższy od statement coverage i to on mówi prawdę o jakości testów.
Wyznaczenie progu
Sam raport niczego nie wymusza. Żeby pokrycie nie osuwało się z każdym tygodniem, ustawiamy próg w konfiguracji Jest:
1coverageThreshold: {
2 global: {
3 statements: 80,
4 branches: 75,
5 functions: 80,
6 lines: 80,
7 },
8},Od tej chwili spadek poniżej progu kończy się błędem - polecenie zwraca kod wyjścia różny od zera, więc zatrzyma też budowanie w CI. Zauważ, że próg dla branches jest niższy niż dla reszty. To nie niedopatrzenie: gałęzi jest z natury trudniej dosięgnąć, więc realistyczny próg ustawia się niżej, żeby wymóg pozostał wykonalny, a nie stał się obrzędem obchodzonym przez wszystkich.
Czego mapa nie pokazuje
Tu dochodzimy do rzeczy, o której łatwo zapomnieć, patrząc na ładne procenty. Coverage mierzy, czy kod się wykonał - nie czy zachował się poprawnie.
Ten test da stuprocentowe pokrycie i nie sprawdza niczego:
1it('oblicza żołd', () => {
2 calculatePay({ rank: 'CENTURION', basePay: 100 });
3});Funkcja została wywołana, więc kartograf zaznaczy drogę jako przebadaną. Ale nie ma tu ani jednego expect - gdyby funkcja zwróciła ujemną liczbę albo rzuciła wyjątkiem, test nadal przeszedłby na zielono. Pokrycie mówi, gdzie zwiadowca był; nie mówi, czy patrzył.
Stąd praktyczna zasada: traktuj coverage jako wykrywacz białych plam, a nie jako ocenę. Niskie pokrycie to twardy sygnał, że czegoś nie sprawdzono - warto tam zajrzeć. Wysokie pokrycie nie dowodzi niczego samo z siebie. Pogoń za okrągłymi stu procentami kończy się zwykle testami pisanymi pod licznik: bez asercji, za to na getterach i plikach konfiguracyjnych.
Dlatego z raportu czytaj przede wszystkim listę nieprzetestowanych linii, a nie sam procent. To ona wskazuje ciemne zaułki, a wśród nich najpierw sprawdzaj obsługę błędów i przypadki brzegowe - bo tam trafiają najrzadziej uruchamiane, a najbardziej kosztowne gałęzie.
Podsumowanie
Mapa Imperium narysowana, białe plamy widoczne:
- test coverage pokazuje, które fragmenty kodu wykonały się podczas testów,
- raport zamawiasz przez
npm run test:cov, czylijest --coverage, - statements i lines to pomiar najgrubszy, functions wyłapuje niewywołane funkcje,
- branch coverage sprawdza ścieżki warunkowe - każde
ifw obu wariantach - i to on najlepiej mówi o jakości testów, - branch coverage jest zawsze niższy od statement coverage, więc jego próg ustawia się niżej,
coverageThresholdw konfiguracji Jest zamienia próg w twardy wymóg: spadek poniżej przerywa budowanie,- coverage mierzy wykonanie, nie poprawność - test bez
expectda pełne pokrycie i nie sprawdzi niczego, - czytaj listę nieprzetestowanych linii, nie sam procent; zaczynaj od obsługi błędów i przypadków brzegowych.
W następnej lekcji zajmiemy się testami wydajnościowymi - sprawdzimy nie to, czy fort działa, ale ile naporu wytrzyma. A na razie zapamiętaj: coverage to mapa z zaznaczonymi drogami zwiadowców - białe plamy mówią, gdzie nikt nie był, ale zaznaczona droga nie dowodzi, że zwiadowca czegokolwiek na niej szukał.
Kod do tej lekcji: src/coverage-example.spec.ts
1// Test Coverage - Mapowanie Przetestowanych Obszarow Fortu
2
3// Serwis do analizy pokrycia
4class TributeCalculator {
5 // Oblicz podatek od prowincji
6 calculateTax(tribute: number, rate: number): number {
7 if (rate < 0 || rate > 1) {
8 throw new Error('Rate must be between 0 and 1');
9 }
10 return tribute * rate;
11 }
12
13 // Kategoryzuj prowincje
14 categorizeProvince(tribute: number): string {
15 if (tribute >= 2000) return 'wealthy';
16 if (tribute >= 1000) return 'standard';
17 if (tribute >= 500) return 'developing';
18 return 'poor';
19 }
20
21 // Oblicz bonus za lojalnosc
22 loyaltyBonus(years: number, baseTribute: number): number {
23 if (years >= 50) return baseTribute * 0.2;
24 if (years >= 20) return baseTribute * 0.1;
25 if (years >= 10) return baseTribute * 0.05;
26 return 0;
27 }
28
29 // Podsumowanie prowincji
30 getSummary(provinces: Array<{ name: string; tribute: number }>) {
31 const total = provinces.reduce((sum, p) => sum + p.tribute, 0);
32 const avg = provinces.length > 0 ? total / provinces.length : 0;
33 const richest = provinces.reduce(
34 (max, p) => (p.tribute > max.tribute ? p : max),
35 provinces[0]
36 );
37 return { total, average: avg, richest: richest?.name };
38 }
39}
40
41// 100% Coverage Tests - kazda linia, kazda galaz
42describe('TributeCalculator - Full Coverage', () => {
43 let calc: TributeCalculator;
44
45 beforeEach(() => {
46 calc = new TributeCalculator();
47 });
48
49 // Branch coverage - kazdy warunek if/else
50 describe('calculateTax', () => {
51 it('should calculate tax correctly', () => {
52 expect(calc.calculateTax(1000, 0.1)).toBe(100);
53 });
54 it('should throw for negative rate', () => {
55 expect(() => calc.calculateTax(1000, -0.1)).toThrow();
56 });
57 it('should throw for rate > 1', () => {
58 expect(() => calc.calculateTax(1000, 1.5)).toThrow();
59 });
60 });
61
62 describe('categorizeProvince', () => {
63 it('wealthy: >= 2000', () => {
64 expect(calc.categorizeProvince(2500)).toBe('wealthy');
65 });
66 it('standard: >= 1000', () => {
67 expect(calc.categorizeProvince(1500)).toBe('standard');
68 });
69 it('developing: >= 500', () => {
70 expect(calc.categorizeProvince(700)).toBe('developing');
71 });
72 it('poor: < 500', () => {
73 expect(calc.categorizeProvince(200)).toBe('poor');
74 });
75 });
76
77 describe('loyaltyBonus', () => {
78 it('50+ years: 20% bonus', () => {
79 expect(calc.loyaltyBonus(60, 1000)).toBe(200);
80 });
81 it('20+ years: 10% bonus', () => {
82 expect(calc.loyaltyBonus(25, 1000)).toBe(100);
83 });
84 it('10+ years: 5% bonus', () => {
85 expect(calc.loyaltyBonus(15, 1000)).toBe(50);
86 });
87 it('< 10 years: no bonus', () => {
88 expect(calc.loyaltyBonus(5, 1000)).toBe(0);
89 });
90 });
91});
92
93// Konfiguracja pokrycia w package.json:
94// "jest": {
95// "collectCoverage": true,
96// "coverageDirectory": "coverage",
97// "coverageThreshold": {
98// "global": {
99// "branches": 80, "functions": 80,
100// "lines": 80, "statements": 80
101// }
102// }
103// }
104Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Metryka 'branch coverage' mierzy:
2. Komenda npm run test:cov generuje:
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.