Lekcja 4.3: Techniki Białoskrzynkowe

W poprzedniej lekcji omówiliśmy techniki czarnoskrzynkowe, które koncentrują się na zewnętrznym zachowaniu systemu.

Teraz czas zajrzeć do środka – do kodu – i poznać techniki białoskrzynkowe (white-box techniques).

Jak sama nazwa wskazuje, techniki te wymagają wiedzy o wewnętrznej strukturze, projekcie i kodzie oprogramowania.

Ich celem jest analiza przepływu sterowania i danych w kodzie, aby zaprojektować testy, które odpowiednio go "przećwiczą".

Sylabus ISTQB Foundation Level v4.0 w sekcji 4.3 (FL-4.3.1 K2, FL-4.3.2 K2, FL-4.3.3 K2) koncentruje się na dwóch podstawowych technikach białoskrzynkowych związanych z kodem oraz zrozumieniu ich wartości:

  1. Testowanie instrukcji (Statement Testing) i pokrycie instrukcji (Statement Coverage)
  2. Testowanie gałęzi (Branch Testing) i pokrycie gałęzi (Branch Coverage)
  3. Korzyści wynikające z testowania białoskrzynkowego

W tej lekcji wyjaśnimy, na czym polegają te techniki, jak mierzy się związane z nimi pokrycie kodu oraz jakie korzyści przynosi stosowanie podejścia białoskrzynkowego w testowaniu.

1. Testowanie Instrukcji i Pokrycie Instrukcji Kodu (Statement Testing and Coverage)

Cel: Zaprojektowanie przypadków testowych, które wykonają każdą wykonywalną instrukcję w kodzie źródłowym co najmniej raz.

Element pokrycia: Wykonywalna instrukcja kodu (np. przypisanie wartości, wywołanie funkcji, operacja arytmetyczna; nie liczą się deklaracje zmiennych, komentarze).

Jak stosować?

  1. Analiza kodu: Zidentyfikuj wszystkie wykonywalne instrukcje w testowanym module lub fragmencie kodu. Często wykorzystuje się do tego narzędzia analizujące kod.
  2. Projektowanie testów: Stwórz zestaw przypadków testowych (danych wejściowych), które spowodują wykonanie każdej zidentyfikowanej instrukcji.
  3. Pomiar pokrycia: Uruchom testy i zmierz, ile instrukcji zostało wykonanych. Pokrycie instrukcji oblicza się według wzoru:
    Pokrycie instrukcji = (Liczba wykonanych instrukcji / Całkowita liczba instrukcji wykonywalnych) * 100%

Przykład:

1. Czytaj A
2. Czytaj B
3. Jeżeli A > B THEN
4.   Drukuj "A jest większe"
5. Inaczej
6.   Drukuj "A nie jest większe"
7. Koniec Jeżeli
8. Koniec

W tym pseudokodzie mamy 6 instrukcji wykonywalnych (1, 2, 3, 4, 6, 8). Instrukcje 5 i 7 to elementy strukturalne, nie wykonywalne linie kodu.

  • Przypadek testowy 1: A = 5, B = 3. Wykonane instrukcje: 1, 2, 3, 4, 8.
  • Przypadek testowy 2: A = 2, B = 4. Wykonane instrukcje: 1, 2, 3, 6, 8.

Aby osiągnąć 100% pokrycia instrukcji, potrzebujemy obu tych przypadków testowych, ponieważ pierwszy wykonuje instrukcję 4, a drugi instrukcję 6.

Po wykonaniu obu testów, wszystkie instrukcje (1, 2, 3, 4, 6, 8) zostały wykonane co najmniej raz.

Wartość i ograniczenia:

  • Wartość: Gwarantuje, że każda linia kodu została uruchomiona, co zwiększa szansę na wykrycie prostych błędów w instrukcjach. Jest to podstawowe, najłatwiejsze do osiągnięcia kryterium pokrycia kodu.
  • Ograniczenia: 100% pokrycia instrukcji nie gwarantuje przetestowania wszystkich możliwych ścieżek decyzyjnych w kodzie. W powyższym przykładzie, nawet jeśli osiągnęliśmy 100% pokrycia instrukcji, nie mamy pewności, czy warunek `A > B` działa poprawnie dla przypadku `A = B`. Testowanie instrukcji nie wykryje też błędów w logice warunków (np. użycie `>` zamiast `>=`). Ponadto, wykonanie instrukcji nie zawsze ujawnia defekt, np. dzielenie przez zero spowoduje błąd tylko wtedy, gdy dzielnik faktycznie będzie zerem.

2. Testowanie Gałęzi i Pokrycie Gałęzi (Branch Testing and Coverage)

Cel: Zaprojektowanie przypadków testowych, które wykonają każdą możliwą gałąź (wynik decyzji) w kodzie co najmniej raz.

Element pokrycia: Gałąź w przepływie sterowania.

Gałąź reprezentuje przepływ sterowania między dwoma punktami w kodzie.

Decyzje (np. instrukcje `IF`, `CASE`, pętle `WHILE`, `FOR`) tworzą punkty rozgałęzień, z których wychodzą co najmniej dwie gałęzie (np. gałąź dla warunku prawdziwego i gałąź dla warunku fałszywego).

Jak stosować?

  1. Analiza kodu i przepływu sterowania: Zidentyfikuj wszystkie punkty decyzyjne w kodzie i wychodzące z nich gałęzie. Często rysuje się graf przepływu sterowania, aby to zwizualizować.
  2. Projektowanie testów: Stwórz zestaw przypadków testowych, które spowodują wykonanie każdej gałęzi (każdego możliwego wyniku każdej decyzji).
  3. Pomiar pokrycia: Uruchom testy i zmierz, ile gałęzi zostało wykonanych. Pokrycie gałęzi oblicza się według wzoru:
    Pokrycie gałęzi = (Liczba wykonanych gałęzi / Całkowita liczba gałęzi) * 100%

Przykład (ten sam co poprzednio):

1. Czytaj A
2. Czytaj B
3. Jeżeli A > B THEN
4.   Drukuj "A jest większe"
5. Inaczej
6.   Drukuj "A nie jest większe"
7. Koniec Jeżeli
8. Koniec

Punkt decyzyjny jest w linii 3 (`Jeżeli A > B`). Mamy dwie gałęzie wychodzące z tej decyzji:

  • Gałąź 1: Warunek `A > B` jest prawdziwy (wykonuje się linia 4).
  • Gałąź 2: Warunek `A > B` jest fałszywy (wykonuje się linia 6).

Aby osiągnąć 100% pokrycia gałęzi, potrzebujemy przypadków testowych, które wykonają obie te gałęzie:

  • Przypadek testowy 1: A = 5, B = 3 (wykonuje gałąź 1).
  • Przypadek testowy 2: A = 2, B = 4 (wykonuje gałąź 2).

W tym prostym przykładzie te same dwa przypadki testowe, które dały 100% pokrycia instrukcji, dają również 100% pokrycia gałęzi.

Jednak rozważmy inny przykład:

1. Czytaj X
2. Jeżeli X > 0 THEN
3.   Drukuj "Dodatnia"
4. Koniec Jeżeli
5. Koniec

Instrukcje wykonywalne: 1, 2, 3, 5.

Gałęzie: Prawda dla `X > 0` (wykonuje linię 3), Fałsz dla `X > 0` (pomija linię 3).

  • Przypadek testowy: X = 5. Wykonane instrukcje: 1, 2, 3, 5. Pokrycie instrukcji: 100%. Wykonana gałąź: Prawda. Pokrycie gałęzi: 50%.

Aby osiągnąć 100% pokrycia gałęzi, potrzebujemy drugiego przypadku, np. X = -1, który wykona gałąź Fałsz.

Wartość i ograniczenia:

  • Wartość: Pokrycie gałęzi jest silniejszym kryterium niż pokrycie instrukcji. 100% pokrycia gałęzi implikuje 100% pokrycia instrukcji (każda instrukcja leży na jakiejś gałęzi). Testowanie gałęzi wymusza sprawdzenie obu wyników każdej decyzji, co zwiększa szansę na wykrycie błędów w logice warunkowej.
  • Ograniczenia: Nadal nie gwarantuje wykrycia wszystkich błędów. Może nie wykryć błędów w złożonych warunkach (np. `IF (A > 0 AND B < 10)` – pokrycie gałęzi wymaga tylko jednego testu dla prawdy i jednego dla fałszu, ale niekoniecznie testuje wszystkie kombinacje A i B). Nie testuje też wszystkich możliwych ścieżek w programie, zwłaszcza w obecności pętli.

3. Korzyści Wynikające z Testowania Białoskrzynkowego

Dlaczego warto „zaglądać do środka” kodu, skoro techniki czarnoskrzynkowe weryfikują wymagania?

Testowanie białoskrzynkowe oferuje kilka istotnych korzyści (FL-4.3.3 K2):

  • Wczesne wykrywanie defektów: Techniki te mogą być stosowane już na etapie kodowania (np. przez programistów piszących testy jednostkowe), co pozwala wykryć i naprawić błędy znacznie wcześniej i taniej niż w późniejszych fazach testów.
  • Maksymalizacja pokrycia kodu: Pozwalają na systematyczne mierzenie i dążenie do wysokiego poziomu pokrycia kodu (instrukcji, gałęzi, a nawet bardziej zaawansowanych kryteriów). Daje to większą pewność, że kluczowe fragmenty logiki zostały przetestowane.
  • Wykrywanie defektów trudnych do znalezienia przez techniki czarnoskrzynkowe: Błędy w specyficznych ścieżkach wykonania, obsłudze błędów wewnętrznych czy złożonej logice warunkowej mogą być trudne do sprowokowania i wykrycia tylko na podstawie analizy zewnętrznego zachowania.
  • Optymalizacja testów: Analiza pokrycia kodu może wskazać obszary, które są niedostatecznie przetestowane, lub wręcz przeciwnie – obszary, gdzie testy są redundantne. Pozwala to na bardziej efektywne alokowanie zasobów testowych.
  • Poprawa jakości kodu: Samo myślenie o tym, jak przetestować kod (np. aby osiągnąć pokrycie gałęzi), często prowadzi programistów do pisania prostszego, bardziej modularnego i łatwiejszego do testowania kodu.
  • Ujawnianie „martwego kodu”: Analiza pokrycia może ujawnić fragmenty kodu, które nigdy nie są wykonywane przez żaden test (a być może w ogóle w aplikacji). Taki „martwy kod” może być usunięty, co upraszcza utrzymanie systemu.

Testowanie białoskrzynkowe jest więc cennym uzupełnieniem testowania czarnoskrzynkowego, dostarczając informacji o wewnętrznej jakości kodu i pomagając zapewnić jego solidność.

Podsumowanie

Techniki białoskrzynkowe, takie jak testowanie instrukcji i gałęzi, koncentrują się na wewnętrznej strukturze kodu.

Ich głównym celem jest zapewnienie odpowiedniego poziomu pokrycia kodu, co zwiększa zaufanie do jego poprawności.

Pokrycie gałęzi jest silniejszym kryterium niż pokrycie instrukcji.

Stosowanie technik białoskrzynkowych, zwłaszcza na niższych poziomach testów (komponentowe, integracyjne), przynosi liczne korzyści, w tym wczesne wykrywanie defektów i poprawę jakości samego kodu.

Najczęściej Zadawane Pytania (FAQ)

Czy 100% pokrycia kodu gwarantuje brak błędów?
Nie. 100% pokrycia (nawet gałęzi) oznacza tylko, że każda instrukcja/gałąź została wykonana co najmniej raz. Nie gwarantuje to przetestowania wszystkich możliwych kombinacji danych, ścieżek czy warunków brzegowych. Możliwe są też błędy w logice, które nie zostaną ujawnione.
Które pokrycie jest lepsze: instrukcji czy gałęzi?
Pokrycie gałęzi jest silniejsze, ponieważ 100% pokrycia gałęzi zawsze implikuje 100% pokrycia instrukcji, ale nie odwrotnie. Testowanie gałęzi wymusza sprawdzenie obu wyników decyzji, co daje większą pewność co do poprawności logiki warunkowej.
Kto zazwyczaj wykonuje testy białoskrzynkowe?
Najczęściej programiści, w ramach testów jednostkowych (komponentowych). Mogą je również wykonywać testerzy techniczni lub testerzy automatyzujący, którzy mają umiejętność czytania i rozumienia kodu oraz dostęp do niego.
Czy można mierzyć pokrycie kodu dla testów czarnoskrzynkowych?
Tak. Można uruchomić zestaw testów systemowych (zaprojektowanych czarnoskrzynkowo) i za pomocą narzędzi zmierzyć, jaki procent kodu (instrukcji, gałęzi) został przez nie pokryty. Może to pomóc zidentyfikować obszary nieprzetestowane przez testy funkcjonalne.
Jakie narzędzia służą do mierzenia pokrycia kodu?
Istnieje wiele narzędzi, często specyficznych dla języka programowania (np. JaCoCo, Cobertura dla Javy; coverage.py dla Pythona; gcov dla C/C++). Integrują się one z procesem budowania i testowania, generując raporty pokrycia.
Czy zawsze należy dążyć do 100% pokrycia kodu?
Niekoniecznie. Osiągnięcie 100% pokrycia (zwłaszcza gałęzi) może być bardzo kosztowne i czasochłonne, a korzyści z pokrycia ostatnich kilku procent mogą być niewielkie. Często ustala się docelowy poziom pokrycia (np. 80-90%) w zależności od krytyczności modułu.
Co to jest "martwy kod"?
Martwy kod (dead code) to fragment kodu źródłowego, który nigdy nie zostanie wykonany podczas normalnego działania programu (np. instrukcje po bezwarunkowym `return` lub `throw`, nieosiągalne gałęzie `if`). Narzędzia do analizy pokrycia mogą pomóc go zidentyfikować.
Czy techniki białoskrzynkowe można stosować do testowania architektury?
Tak, choć sylabus FL skupia się na kodzie. Na wyższych poziomach można analizować przepływ danych i sterowania między komponentami architektury, projektując testy integracyjne w oparciu o tę strukturę.
Czy testowanie białoskrzynkowe zastępuje testowanie czarnoskrzynkowe?
Nie, one się uzupełniają. Testowanie białoskrzynkowe sprawdza, czy kod jest zbudowany poprawnie, a czarnoskrzynkowe – czy robi to, co powinien zgodnie z wymaganiami. Potrzebne są oba podejścia dla kompleksowej oceny jakości.
Czy istnieją inne, bardziej zaawansowane kryteria pokrycia kodu?
Tak, istnieją silniejsze kryteria, np. pokrycie warunków (condition coverage), pokrycie zmodyfikowanych warunków/decyzji (MC/DC - Modified Condition/Decision Coverage, ważne w lotnictwie), pokrycie ścieżek (path coverage). Są one omawiane na wyższych poziomach ISTQB.

Przykładowe Pytania Egzaminacyjne

Pytanie 1 (K2): Które z poniższych stwierdzeń dotyczących pokrycia instrukcji i pokrycia gałęzi jest PRAWDZIWE?

  • a) 100% pokrycia instrukcji gwarantuje 100% pokrycia gałęzi.
  • b) 100% pokrycia gałęzi gwarantuje 100% pokrycia instrukcji.
  • c) Pokrycie instrukcji jest silniejszym kryterium niż pokrycie gałęzi.
  • d) Pokrycie gałęzi mierzy, ile linii kodu zostało wykonanych.

Poprawna odpowiedź: b (Jeśli każda gałąź została wykonana, to każda instrukcja (która leży na jakiejś gałęzi) również musiała zostać wykonana. Odwrotna implikacja nie jest prawdziwa.)

Pytanie 2 (K1): Jaki jest główny cel testowania gałęzi?

  • a) Wykonanie każdej linii kodu co najmniej raz.
  • b) Przetestowanie wszystkich możliwych wartości wejściowych.
  • c) Wykonanie każdego możliwego wyniku każdej decyzji w kodzie co najmniej raz.
  • d) Sprawdzenie zgodności z wymaganiami użytkownika.

Poprawna odpowiedź: c (Testowanie gałęzi koncentruje się na wykonaniu wszystkich możliwych przepływów sterowania wynikających z punktów decyzyjnych w kodzie.)

Pytanie 3 (K2): Która z poniższych jest korzyścią wynikającą ze stosowania technik białoskrzynkowych?

  • a) Są łatwe do zastosowania bez znajomości kodu.
  • b) Pozwalają maksymalizować pokrycie wymagań biznesowych.
  • c) Pomagają w wykrywaniu defektów na wczesnym etapie rozwoju.
  • d) Koncentrują się wyłącznie na zewnętrznym zachowaniu systemu.

Poprawna odpowiedź: c (Testy białoskrzynkowe, często stosowane jako testy jednostkowe przez programistów, umożliwiają wczesne wykrywanie błędów w kodzie.)