Techniki projektowania testów — biała skrzynka

Testowanie białoskrzynkowe wykorzystuje strukturę kodu do projektowania testów. Poznaj pokrycie instrukcji, decyzji i ścieżek — i dlaczego 100% pokrycia nie oznacza braku bugów.

📚 To jest Część 7 serii ISTQB Poziom Podstawowy. W poprzednim artykule omawialiśmy techniki projektowania testów czarnoskrzynkowych. Teraz zajrzymy do środka skrzynki.

1. Wprowadzenie — Zajrzenie do środka skrzynki

W poprzednim artykule traktowaliśmy system jak czarną skrzynkę: dane wchodzą, dane wychodzą, a my nie patrzymy na wnętrze. Techniki czarnoskrzynkowe są potężne właśnie dlatego, że działają na podstawie specyfikacji — mówią, co system powinien robić.

Ale testowanie czarnoskrzynkowe ma słaby punkt. Może testować tylko to, co specyfikacja opisuje. Jeśli w implementacji jest kod bez odpowiadającego wymagania — zapomniana wartość brzegowa, martwy kod, nieudokumentowane zachowanie domyślne — testy czarnoskrzynkowe nigdy do niego nie dotrą.

Tu wkracza testowanie białoskrzynkowe. Zamiast wyprowadzać testy z wymagań, wyprowadzasz je ze struktury samego kodu. Skrzynka jest teraz przezroczysta — widać każdą instrukcję, każdą gałąź, każdą ścieżkę.

Kluczowe różnice:

WymiarCzarna skrzynkaBiała skrzynka
PodstawaWymagania / specyfikacjeStruktura kodu
Wymaga dostępu do koduNieTak
Typowy poziom testówSystemowe, akceptacyjneJednostkowe, integracyjne
WykrywaDefekty behawioralneDefekty strukturalne
Miara pokryciaPokryte scenariuszeWykonane elementy kodu

Testowanie białoskrzynkowe jest stosowane głównie na poziomie testów jednostkowych i testów integracyjnych, gdzie programiści piszą testy do własnego kodu. Jest rzadziej stosowane na wyższych poziomach testów, gdzie implementacja może obejmować wiele usług.

ISTQB Poziom Podstawowy definiuje trzy kryteria pokrycia białoskrzynkowego:

  1. Pokrycie instrukcji
  2. Pokrycie gałęzi (decyzji)
  3. Pokrycie ścieżek

Przez cały artykuł będziemy używać jednego przykładu kodu, aby porównanie było konkretne.


Przykład przewodni

Przez cały artykuł będziemy używać tej pseudokodowej funkcji rabatu:

function discount(price, age):
    if age >= 65:
        return price * 0.8      // 20% rabat dla seniora
    if price > 100:
        return price * 0.9      // 10% rabat za wysoką wartość
    return price                // brak rabatu

Ta funkcja ma:

  • 5 wykonywalnych instrukcji (oba warunki if liczą się jako instrukcje, plus trzy instrukcje return)
  • 3 gałęzie (Prawda/Fałsz dla każdego if oraz przejście do ostatniego return)
  • Wiele różnych ścieżek w zależności od tego, które warunki są prawdziwe

2. Pokrycie instrukcji (ISTQB 4.3.1)

Koncepcja

Pokrycie instrukcji mierzy, czy każda wykonywalna instrukcja w kodzie została wykonana przynajmniej raz.

Wzór: Pokrycie instrukcji = (Wykonane instrukcje ÷ Łączna liczba instrukcji) × 100%

Zestaw testów osiągający 100% pokrycia instrukcji uruchomił każdą linię kodu przynajmniej raz.

Zastosowanie do naszego przykładu

Funkcja discount ma 5 wykonywalnych instrukcji. Ile przypadków testowych potrzebujemy, aby wykonać je wszystkie?

Przypadek testowy 1: price = 120, age = 70

  • if age >= 65 → Prawda
  • return price * 0.8 ✅ wykonano
  • Wynik: 96

Przypadek testowy 2: price = 120, age = 30

  • if age >= 65 → Fałsz
  • if price > 100 → Prawda
  • return price * 0.9 ✅ wykonano
  • Wynik: 108

Przypadek testowy 3: price = 50, age = 30

  • if age >= 65 → Fałsz
  • if price > 100 → Fałsz
  • return price ✅ wykonano
  • Wynik: 50

Wszystkie 5 instrukcji wykonanych. Pokrycie instrukcji: 100%.

Ograniczenia

Pokrycie instrukcji jest najsłabszym kryterium białoskrzynkowym. Rozważ co się dzieje przy uruchomieniu tylko Przypadków testowych 1 i 2: pokrycie instrukcji nadal wynosi 100%, ale nigdy nie przetestowaliśmy przypadku gdy age < 65 AND price <= 100.

Co gorzej: możesz wykonać każdą instrukcję return, całkowicie pomijając gałąź Fałsz sprawdzenia price > 100. Pokrycie instrukcji nie daje żadnej informacji o nieprzetestowanych wynikach decyzji.

:::tip[Wskazówka egzaminacyjna ISTQB] Pokrycie instrukcji to minimum, nie cel jakościowy. ISTQB traktuje je jako najsłabsze kryterium białoskrzynkowe. 100% pokrycia instrukcji można osiągnąć, pomijając wiele ważnych ścieżek decyzyjnych. :::


3. Pokrycie gałęzi / decyzji (ISTQB 4.3.2)

Koncepcja

Pokrycie gałęzi (zwane też pokryciem decyzji) wymaga, aby każda gałąź każdego punktu decyzji została wykonana — zarówno wynik Prawda, jak i wynik Fałsz każdego if, while, switch itp.

Pokrycie gałęzi obejmuje pokrycie instrukcji: osiągnięcie 100% pokrycia gałęzi automatycznie daje 100% pokrycia instrukcji. Odwrotność nie jest prawdą.

Zastosowanie do naszego przykładu

Funkcja discount ma dwie decyzje if, każda wymagająca przypadku testowego dla Prawdy i dla Fałszu:

Przypadek testowy 1: price = 120, age = 70

  • if age >= 65Prawda

Przypadek testowy 2: price = 120, age = 30

  • if age >= 65Fałsz
  • if price > 100Prawda

Przypadek testowy 3: price = 50, age = 30

  • if age >= 65 → Fałsz (już pokryte)
  • if price > 100Fałsz

Trzy przypadki testowe osiągają 100% pokrycia gałęzi. Co kluczowe, pokrycie gałęzi gwarantuje przetestowanie co się dzieje gdy każdy warunek jest fałszywy — czego samo pokrycie instrukcji nie zapewnia.

Praktyka branżowa

Pokrycie gałęzi jest de facto standardem dla testów jednostkowych. Narzędzia pomiarowe są dostępne dla każdej głównej platformy:

  • C#/.NET: Coverlet — uruchamiany przez dotnet test --collect:"XPlat Code Coverage"
  • JavaScript/TypeScript: Istanbul / NYC — zintegrowany z Jest i Vitest
  • Java: JaCoCo
  • Python: coverage.py

Większość zespołów egzekwuje minimum pokrycia gałęzi (zazwyczaj 70–80%) jako bramkę jakości w CI/CD.

:::tip[Wskazówka egzaminacyjna ISTQB] ISTQB używa terminu “pokrycie decyzji” dla tego, co programiści zazwyczaj nazywają “pokryciem gałęzi” — oznaczają to samo. Kluczowy fakt egzaminacyjny: pokrycie gałęzi/decyzji obejmuje pokrycie instrukcji. Przy 100% pokrycia gałęzi automatycznie masz 100% pokrycia instrukcji. :::


4. Pokrycie ścieżek (ISTQB 4.3.3)

Koncepcja

Pokrycie ścieżek wymaga, aby każda możliwa ścieżka wykonania przez kod została przetestowana. “Ścieżka” to unikalna sekwencja gałęzi od wejścia do wyjścia funkcji.

Dla funkcji discount istnieją trzy różne ścieżki:

  1. age >= 65 jest Prawdą → return price * 0.8
  2. age >= 65 jest Fałszem → price > 100 jest Prawdą → return price * 0.9
  3. age >= 65 jest Fałszem → price > 100 jest Fałszem → return price

Trzy przypadki testowe pokrywają wszystkie ścieżki w tej małej funkcji. Ale rozważ co się dzieje przy bardziej złożonym kodzie.

Dlaczego pokrycie ścieżek jest zazwyczaj niepraktyczne

Liczba ścieżek rośnie wykładniczo wraz z liczbą punktów decyzji:

Punkty decyzjiMaksymalna liczba ścieżek
532
101 024
201 048 576
Dowolna pętlaPotencjalnie nieskończona

Dla każdej nietrywialnej funkcji 100% pokrycia ścieżek jest kombinatorycznie niewykonalne. To teoretyczny ideał, nie praktyczny cel.

MC/DC — Praktyczna alternatywa

W systemach o krytycznym znaczeniu dla bezpieczeństwa (lotnictwo wg DO-178C, motoryzacja wg ISO 26262) kryterium zwane Modified Condition/Decision Coverage (MC/DC) zapewnia znaczną część rygorystyczności pokrycia ścieżek przy ułamku liczby testów.

MC/DC wymaga, aby każdy indywidualny warunek w decyzji niezależnie wpływał na wynik. ISTQB Poziom Podstawowy wymaga jedynie świadomości istnienia MC/DC; jest ono szczegółowo omawiane na Poziomie Zaawansowanym.

:::tip[Wskazówka egzaminacyjna ISTQB] Na egzaminie Poziomu Podstawowego zapamiętaj hierarchię: pokrycie ścieżek jest najsilniejszym, ale najmniej praktycznym kryterium. ISTQB oczekuje znajomości zależności: Instrukcje ⊂ Gałęzie ⊂ Ścieżki — każde kryterium obejmuje poprzednie. :::


5. Mit pokrycia kodu

Oto zdanie, które często usłyszysz w zespołach programistycznych:

“Mamy 80% pokrycia kodu. Jesteśmy bezpieczni.”

Sprawdźmy, dlaczego może to być mniej uspokajające niż brzmi.

:::danger[Krytyczne nieporozumienie] 100% pokrycia kodu nie oznacza, że kod jest poprawny. Pokrycie mierzy które linie zostały wykonane, a nie czy zachowanie zostało zweryfikowane. Można osiągnąć 100% pokrycia instrukcji bez żadnych asercji i nie wykryć żadnych błędów. :::

Rozważ ten zestaw testów:

// Osiąga 100% pokrycia instrukcji. Nie wykrywa żadnych błędów.
test("funkcja discount uruchamia się bez błędów") {
    discount(120, 70);   // brak asercji — wynik ignorowany
    discount(120, 30);   // brak asercji — wynik ignorowany
    discount(50, 30);    // brak asercji — wynik ignorowany
}

Każda instrukcja jest wykonana. Żadne zachowanie nie jest weryfikowane. Funkcja mogłaby zwracać nieprawidłowe wartości, a ten zestaw testów przeszłby bez problemu.

Co naprawdę mówi pokrycie

Pokrycie to narzędzie diagnostyczne, nie miara jakości:

  • Wysokie pokrycie informuje: kod był wykonywany podczas testowania
  • Wysokie pokrycie NIE informuje: czy zachowanie było poprawne, czy przetestowano wszystkie scenariusze, czy asercje były znaczące

Pokrycie najlepiej służy do znajdowania luk — obszarów kodu, które nigdy nie były wykonywane, co sugeruje brakujące testy. Jest użyteczną podłogą (“powinniśmy przynajmniej wykonać każdą gałąź”), ale mylącym sufitem (“gdy wykonamy każdą gałąź, testowanie jest skończone”).

Używanie pokrycia jako KPI tworzy przewrotne motywacje: zespoły piszą testy wykonujące kod bez weryfikowania zachowania, wyłącznie po to, by osiągnąć docelowy wskaźnik.

Lepsze ujęcie: Pytaj “jakie scenariusze pokrywa nasz zestaw testów?” obok pytania “jaki procent gałęzi wykonuje?”. Oba pytania są ważne; żadne z nich nie jest samodzielnie wystarczające.


6. Łączenie testowania białoskrzynkowego i czarnoskrzynkowego

Najskuteczniejsza strategia testowania łączy oba podejścia, stosowane na właściwych poziomach.

Zalecany przepływ pracy:

  1. Napisz najpierw testy czarnoskrzynkowe (na podstawie wymagań): Wyprowadź przypadki testowe ze specyfikacji używając EP, BVA, tablic decyzyjnych i przejść stanów. Testy te są niezależne od implementacji — pozostają ważne, nawet jeśli kod zostanie całkowicie przepisany.

  2. Uruchom analizę pokrycia (obiektyw białoskrzynkowy): Po napisaniu testów czarnoskrzynkowych zmierz pokrycie gałęzi. Każdy kod niepokryty przez testy czarnoskrzynkowe sygnalizuje lukę: brakujące wymaganie, nieprzetestowany przypadek brzegowy lub martwy kod.

  3. Dodaj ukierunkowane testy białoskrzynkowe dla luk strukturalnych: Napisz dodatkowe testy celujące w niepokryte gałęzie i ścieżki. Testy te są specyficzne dla implementacji, ale zapewniają faktyczne przetestowanie kodu.

Ten przepływ pracy tworzy zestaw testów, który jest napędzany wymaganiami (każdy test ma jasny powód istnienia) i strukturalnie kompletny (implementacja jest faktycznie testowana).

Typowe podejście według poziomów testów:

Poziom testówGłówne podejścieCel pokrycia
Testy jednostkoweBiała + Czarna skrzynkaPokrycie gałęzi ≥ 70–80%
Testy integracyjneCzarna skrzynka (głównie)Pokrycie instrukcji
Testy systemoweCzarna skrzynka (wyłącznie)Pokrycie scenariuszy
Testy akceptacyjneCzarna skrzynka (wyłącznie)Pokrycie scenariuszy biznesowych

7. Podsumowanie

Techniki białoskrzynkowe zapewniają uzupełniającą perspektywę względem testowania czarnoskrzynkowego — gwarantują testowanie implementacji, a nie tylko specyfikacji.

Hierarchia pokrycia:

Pokrycie ścieżek        (najsilniejsze — niepraktyczne dla prawdziwego kodu)
        ↑ obejmuje
Pokrycie gałęzi / decyzji    (standard branżowy dla testów jednostkowych)
        ↑ obejmuje
Pokrycie instrukcji    (minimalna linia bazowa)

Kluczowy wniosek: Pokrycie to narzędzie diagnostyczne, nie gwarancja. 80% pokrycia gałęzi ze znaczącymi asercjami jest dużo wartościowsze niż 100% pokrycia instrukcji bez żadnych.

Działanie: Uruchom raport pokrycia dla bieżącego projektu (dotnet test --collect:"XPlat Code Coverage" lub jest --coverage). Znajdź pięć funkcji z najniższym pokryciem gałęzi. Dla każdej z nich zapytaj: czy ta luka jest zamierzona, czy jest scenariusz wart przetestowania?


To jest Część 7 serii ISTQB Poziom Podstawowy.