Poglavlje 6: Tehnike bijele kutije
Priča za početak. U petak, 21. februara 2014., Apple je izdao malu, neobičnu ispravku za iPhone i iPad: iOS 7.0.6. Opis je imao jednu rečenicu: napadač „u privilegovanom položaju na mreži” mogao je presresti i promijeniti podatke koji su trebali biti zaštićeni. Programeri širom svijeta odmah su otvorili javno objavljen izvorni kod Appleove biblioteke za sigurne veze (SSL/TLS) i za nekoliko sati našli uzrok. U funkciji koja provjerava digitalni potpis servera stajala su, jedan ispod drugog, dva ista reda:
goto fail;. Prvi je bio ispravan i pripadao je naredbiif. Drugi je bio višak, vjerovatno nastao pri spajanju izmjena, i izvršavao se uvijek. Program je zato skakao na kraj funkcije prije nego što bi uopšte provjerio potpis, a promjenljivaerrje u tom trenutku imala vrijednost 0, što znači „sve u redu”. Telefon je tako prihvatao lažni server kao pravi: napadač na istoj Wi-Fi mreži u kafiću mogao je glumiti banku ili poštu. Ranjivost je dobila oznaku CVE-2014-1266 i nadimak „goto fail”, a ispravka za Mac računare (OS X 10.9.2) stigla je nekoliko dana kasnije. Najzanimljivije za ovo poglavlje je jedno pitanje koje su tada postavili mnogi: kako niko nije primijetio da se provjera potpisa nikad ne izvrši? Alat koji broji koji su redovi koda izvršeni u testovima pokazao bi da redovi za provjeru potpisa nisu izvršeni nijednom. Taj alat se zove mjerenje pokrivenosti koda, i to je glavna tema ovog poglavlja.

U poglavljima 3 do 5 test smo pravili iz pravila: PP-2 kaže koliko košta dostava, pa test provjeri tu cijenu. Nismo gledali unutra, u kod. Tako se radi crnom kutijom i detaljno ćemo je učiti u poglavlju 8. Ovo poglavlje otvara poklopac. Kod cijena_dostave koji smo testirali u poglavljima 4 i 5 sada crtamo kao graf, brojimo njegove puteve i mjerimo koliko ga naši testovi stvarno obilaze. Prvo učimo graf toka kontrole i ciklomatsku složenost. Zatim kriterije pokrivenosti: iskaze, grane, uslove i MC/DC. Na kraju mjerimo pokrivenost alatom coverage.py i vidimo njegovu najvažniju pouku: 100 % pokrivenosti ne znači da grešaka nema.
Literatura. Osnovna literatura predmeta:
- J. Popović: Priručnik za test inženjere, CET i Računarski fakultet, Beograd, 2023.
- Rakić-Skoković: Priručnik za testiranje softvera, Fakultet tehničkih nauka, Novi Sad, 2013.
- G. J. Myers: The Art of Software Testing, 2. izdanje, John Wiley & Sons, 2004.
- A. P. Mathur: Foundations of Software Testing, Addison-Wesley Professional, 2008.
Za ovo poglavlje: T. J. McCabe: „A Complexity Measure”, IEEE Transactions on Software Engineering, 1976; P. Ammann, J. Offutt: Introduction to Software Testing, 2nd ed., Cambridge University Press, 2016; dokumentacija alata coverage.py (coverage.readthedocs.io).
Riječi ovog poglavlja.
- Bijela kutija (white-box, structural testing) – testovi koji se prave i ocjenjuju gledajući kod iznutra.
- Graf toka kontrole (control flow graph, CFG) – crtež programa: čvorovi su dijelovi koda, strelice su mogući skokovi s jednog na drugi.
- Odluka (decision) – mjesto gdje program bira put, npr.
if; ima ishod DA ili NE.- Uslov (condition) – najmanji logički dio odluke; odluka
km < 0 or km > 10ima dva uslova.- Ciklomatska složenost V(G) – broj nezavisnih puteva kroz funkciju; računa se kao broj odluka + 1.
- Bazni put (basis path) – jedan od V(G) puteva od kojih se mogu „sklopiti” svi ostali.
- Pokrivenost (coverage) – procenat dijelova koda (redova, grana, uslova) koje su testovi izvršili.
- Pokrivenost iskaza (statement coverage) – koliko je redova koda izvršeno bar jednom.
- Pokrivenost grana (branch coverage) – koliko je ishoda odluka (DA i NE) izvršeno.
- MC/DC (Modified Condition/Decision Coverage) – svaki uslov mora bar jednom sam, bez pomoći drugih, promijeniti ishod odluke.
- Nedostižan kod (dead code, unreachable code) – kod do kojeg program nikad ne može doći.
- coverage.py – alat za mjerenje pokrivenosti Python koda.
Ciljevi poglavlja. Nakon poglavlja možete: objasniti razliku između bijele i crne kutije; nacrtati graf toka kontrole za kratku funkciju; izračunati ciklomatsku složenost na dva načina i napisati bazne puteve; razlikovati pokrivenost iskaza, grana, uslova, MC/DC i putanja i reći koja uključuje koju; za odluku s dva uslova napraviti najmanji skup testova za MC/DC; izmjeriti pokrivenost alatom coverage.py i pročitati kolonu „Missing”; dopisati testove tako da pokrivenost grana poraste do 100 %; objasniti zašto 100 % pokrivenosti ne garantuje kod bez grešaka.
Plan poglavlja – Sedmica 6: tri časa teorije i tri časa praktične nastave
| Čas | Sadržaj | Na času |
|---|---|---|
| Teorija 1 | 6.1–6.3 Bijela kutija; graf toka kontrole; ciklomatska složenost i bazni putevi | Graf funkcije cijena_dostave na tabli |
| Teorija 2 | 6.4–6.7 Pokrivenost iskaza, grana, uslova, MC/DC i putanja | Koliko testova treba za MC/DC? |
| Teorija 3 | 6.8–6.10 coverage.py; od 62 % do 100 %; zašto 100 % nije dovoljno | Greška D-01 koja preživi 100 % |
| Praksa 1–3 | Mjerenje pokrivenosti DeliveryApp; dopisivanje testova; HTML izvještaj | primjeri/pokrivenost/ |
Dio 1 – Kod kao mapa
6.1 Bijela i crna kutija
Zamislite da testirate auto. Možete sjesti, voziti i provjeriti da li koči, da li skreće i koliko troši. Ne znate šta je ispod haube, ali znate šta auto treba da radi. To je crna kutija: test se pravi iz zahtjeva, a unutrašnjost se ne gleda. Druga mogućnost je da otvorite haubu, kao mehaničar, i provjerite svaku cijev i svaki kabl: da li je svaki dio bar jednom proradio. To je bijela kutija (ponekad i „staklena kutija”): test se pravi i ocjenjuje gledajući kod.
Dvije kutije odgovaraju na različita pitanja:
| Crna kutija | Bijela kutija | |
|---|---|---|
| Izvor testova | zahtjevi i pravila (PP-1 … PP-6) | kod: odluke, grane, putevi |
| Pitanje | Radi li program ono što treba? | Jesu li testovi obišli sav kod? |
| Hvata | pogrešno ponašanje, zaboravljen slučaj iz pravila | kod koji nijedan test ne izvršava, mrtav kod, zaboravljenu granu |
| Ne hvata | kod koji radi nešto što pravilo ne spominje | pravilo koje kod uopšte nije implementirao |
| Ko najčešće radi | tester, korisnik, analitičar | programer, uz jedinične testove |
Posljednji red tabele je važan. Bijela kutija ne može naći ono čega u kodu nema. Ako je programer zaboravio pravilo PP-4 (besplatna dostava), u kodu nema odluke iznos >= 3000, pa je nema ni u grafu, i bijela kutija ne vidi da nešto fali. Zato se dvije kutije dopunjuju: crnom kutijom napravimo testove iz pravila, a bijelom provjerimo da li su ti testovi obišli sav kod.
Slika iz života: pokrivenost je kao obilazak grada. Poštar koji kaže „obišao sam grad” može misliti na glavnu ulicu. Mapa na kojoj su obojene ulice kojima je zaista prošao odmah pokazuje koje ulice nije vidio. Graf toka kontrole je mapa, a alat za pokrivenost boji ulice.
Ukratko.
- Crna kutija pravi testove iz zahtjeva, a bijela kutija pravi i ocjenjuje testove gledajući kod.
- Bijela kutija pokazuje koji dijelovi koda nisu izvršeni ni u jednom testu.
- Bijela kutija ne može naći pravilo koje uopšte nije programirano, jer ga nema u kodu.
- Dvije kutije se dopunjuju: testovi iz pravila, provjereni mjerenjem pokrivenosti.
6.2 Graf toka kontrole
Da bismo brojali puteve kroz kod, prvo ga crtamo. Graf toka kontrole ima:
- čvorove (kružiće): komad koda koji se izvršava bez skretanja, ili jednu odluku;
- grane (strelice): moguće skokove s jednog komada na drugi;
- jedan ulaz i jedan izlaz (kraj funkcije, bilo da je
returniliraise).
Naša funkcija iz primjeri/pokrivenost/dostava.py je ista kao u DeliveryApp, samo s brojevima umjesto konstanti, da se lakše čita:
def cijena_dostave(udaljenost_km, iznos):
if udaljenost_km < 0 or udaljenost_km > 10: # red 6
raise ValueError("udaljenost izvan zone dostave") # red 7
if iznos < 500: # red 8
raise ValueError("iznos manji od minimalne narudžbe") # red 9
if iznos >= 3000: # red 10
return 0 # red 11
dodatni_km = math.ceil(max(0, udaljenost_km - 3)) # red 12
return 200 + 50 * dodatni_km # red 13Crtamo ga korak po korak (slika 6.2):
- Čvor 1 je prva odluka (red 6). Ako je DA, ide se u čvor 2 (
raise, red 7) i funkcija završava. Ako je NE, ide se dalje. - Čvor 3 je druga odluka (red 8). DA vodi u čvor 4 (
raise, red 9), NE vodi dalje. - Čvor 5 je treća odluka (red 10). DA vodi u čvor 6 (
return 0), NE u čvor 7. - Čvor 7 su redovi 12 i 13 zajedno: izvršavaju se uvijek jedan za drugim, bez skretanja, pa su jedan čvor.
- Svi završeci (2, 4, 6, 7) vode u zajednički čvor kraj.

cijena_dostave: tri odluke (narandžasto), četiri završetkaBrojimo: 8 čvorova (1–7 i kraj) i 10 grana (šest izlaza iz odluka i četiri strelice prema kraju).
Na času. Nacrtajte graf za funkciju
popust(iznos, kod)izdeliveryapp.py:if not kod: return 0, paprocenat = …, paif procenat is None: return 0, pareturn min(…). Koliko čvorova i grana ima?
Odgovor: ulaz s prvom odlukom (1), return 0 (2), red s procenat i druga odluka (3), return 0 (4), return min (5) i kraj (6): 6 čvorova; grane 1→2, 1→3, 3→4, 3→5, 2→kraj, 4→kraj, 5→kraj: 7 grana.
Ukratko.
- Graf toka kontrole crta program kao čvorove (komade koda i odluke) i grane (moguće skokove).
- Redovi koji se uvijek izvršavaju jedan za drugim čine jedan čvor.
- Svaka odluka
ifima dvije izlazne grane, DA i NE.- Graf funkcije
cijena_dostaveima 8 čvorova i 10 grana.
6.3 Ciklomatska složenost i bazni putevi
Koliko testova treba da bismo „prošli” svaki dio grafa? Odgovor daje ciklomatska složenost, mjera koju je Thomas McCabe predložio 1976. godine. Ona broji nezavisne puteve kroz funkciju. Postoje tri načina da se izračuna, i svi daju isti broj:
\[V(G) = E - N + 2\]
gdje je E broj grana, a N broj čvorova. Za cijena_dostave: V(G) = 10 − 8 + 2 = 4.
Drugi način je lakši za glavu: V(G) = broj odluka + 1. Funkcija ima tri if, pa je V(G) = 3 + 1 = 4.
Treći način je za one koji vole crtati: broj zatvorenih oblasti u grafu + 1 (oblasti koje strelice ograđuju kad se graf nacrta bez presijecanja).
Šta broj 4 znači u praksi? Postoje 4 bazna puta, a svaki drugi put kroz funkciju može se „sklopiti” od njih. Ako testovi prođu sva četiri, svaka grana grafa izvršena je bar jednom.
| Put | Čvorovi | Ulaz testa | Očekivano | Pravilo |
|---|---|---|---|---|
| P1 | 1 → 2 → kraj | −1 km, 10,00 € | ValueError „izvan zone” | PP-3 |
| P2 | 1 → 3 → 4 → kraj | 5 km, 4,50 € | ValueError „minimalne” | PP-1 |
| P3 | 1 → 3 → 5 → 6 → kraj | 2,5 km, 32,00 € (N-1002) | 0,00 € | PP-4 |
| P4 | 1 → 3 → 5 → 7 → kraj | 4,2 km, 21,50 € (N-1001) | 3,00 € | PP-2 |
Primijetite da svaki red ima očekivani rezultat iz pravila, a ne iz koda. Graf nam kaže koje puteve treba proći, a pravilo kaže šta treba dobiti na kraju puta.
Česta zabluda. „Odluka
km < 0 or km > 10je jedanif, dakle jedna odluka.” Za graf s jednim čvorom poifto je tačno i V(G) = 4. Aliorkrije dva uslova, i ako graf crtamo tako da svaki uslov ima svoj čvor (kako kompajler zaista radi), dobijamo V(G) = 5. Alati se razlikuju: neki brojeoriand, neki ne. Važno je da u timu svi broje isto.
Zašto testeru treba V(G)?
- Najmanji broj testova za puno obilaženje grana. Za V(G) = 4 trebaju nam bar četiri testa (osim kad jedan test slučajno prođe dva puta, što nije moguće u funkciji s
raise). - Upozorenje na složen kod. McCabe je predložio da funkcija ne prelazi 10. Funkcija s V(G) = 25 ima 25 nezavisnih puteva; teško je razumjeti, teško testirati i lako je pokvariti. Alat ruff iz poglavlja 7 ima pravilo C901 koje javlja funkcije složenije od zadate granice.
- Mjesto za refaktorisanje. Složenu funkciju često je bolje razbiti na nekoliko manjih, svaku s malim V(G), nego pisati 25 testova za jednu.
Broj za pamćenje. V(G) = broj odluka + 1. Preko 10 je znak da funkciju treba podijeliti.
Ukratko.
- Ciklomatska složenost V(G) broji nezavisne puteve kroz funkciju: V(G) = E − N + 2 = broj odluka + 1.
- Za
cijena_dostaveV(G) je 4, ili 5 ako se uslovi uorbroje posebno.- Četiri bazna puta daju četiri testa koji zajedno izvrše svaku granu grafa.
- Očekivani rezultat na kraju puta uvijek dolazi iz pravila, a ne iz koda.
- Funkcija s V(G) većim od 10 teško se testira i obično je treba podijeliti.
Provjeri se.
- Šta je čvor, a šta grana u grafu toka kontrole? – Čvor je komad koda bez skretanja ili jedna odluka; grana je mogući skok s jednog čvora na drugi.
- Kolika je V(G) funkcije s pet naredbi
ifi bez petlji? – 5 + 1 = 6.- Koji test prolazi bazni put P3 funkcije
cijena_dostave? – Narudžba od 30,00 € ili više u zoni, npr. N-1002: 2,5 km i 32,00 €, očekivano 0,00 €.
Dio 2 – Kriteriji pokrivenosti
6.4 Pokrivenost iskaza
Najjednostavnije pitanje je: koliko redova koda je izvršeno bar jednom?
\[\text{pokrivenost iskaza} = \frac{\text{izvršeni iskazi}}{\text{svi iskazi}} \times 100\,\%\]
Iskaz (statement) je jedna naredba: dodjela, if, return, raise, poziv funkcije. Alat coverage.py u fajlu dostava.py broji 10 iskaza: import math, def, tri if, dva raise, return 0, red s dodatni_km i posljednji return. (Red def i import se „izvrše” kad se modul učita.)
Uzmimo samo jedan test, N-1001: 4,2 km i 21,50 €. Kroz koje redove prolazi? Red 6 (NE), red 8 (NE), red 10 (NE), red 12, red 13. Uz import i def to je 7 od 10 iskaza, dakle 70 %. Nisu izvršeni redovi 7, 9 i 11: oba raise i return 0.
Drugi test, N-1002 (2,5 km, 32,00 €), prolazi i kroz red 11. Sada je izvršeno 8 od 10, dakle 80 %, a nedostaju samo redovi 7 i 9: dvije poruke o grešci nikad nisu provjerene.
Pokrivenost iskaza je ono što bi otkrilo goto fail. U Appleovom kodu redovi s provjerom potpisa poslije duplog goto fail; nisu se izvršavali ni u jednom testu, ni u jednom stvarnom pozivu. Izvještaj o pokrivenosti bi te redove pokazao crveno, s nula izvršenja (slika 6.1). Isti problem mogu pokazati i upozorenja kompajlera za nedostižan kod, ali samo ako su uključena.
Iz stvarnog svijeta. Nakon goto fail mnogi su istakli da bi test koji šalje namjerno lažan potpis odmah pokazao da ga telefon prihvata. Takav test je crna kutija (iz pravila: „lažan potpis mora biti odbijen”), a mjerenje pokrivenosti je bijela kutija (iz koda: „ovaj red nikad nije izvršen”). Svaki od ta dva pristupa sam bi uhvatio grešku. Nijedan nije bio primijenjen.
Ukratko.
- Pokrivenost iskaza je procenat naredbi koje su testovi izvršili bar jednom.
- Test N-1001 sam izvrši 7 od 10 iskaza funkcije
cijena_dostave, a s N-1002 to je 8 od 10.- Redovi koji se nikad ne izvrše vide se odmah, kao provjera potpisa u goto fail.
- Pokrivenost iskaza je najslabiji kriterij, ali je bolji nego nikakav.
6.5 Pokrivenost grana (odluka)
Pokrivenost iskaza ima slabu tačku: if bez else. Pogledajte mali primjer iz primjeri/pokrivenost/besplatno.py:
def dostava_s_kodom(cijena, kod):
if kod == "BESPLATNO":
cijena = 0
return cijenaJedan test, dostava_s_kodom(300, "BESPLATNO") == 0, izvrši sva četiri iskaza: 100 % pokrivenost iskaza. Ali nikad nismo provjerili šta se desi kad kod nije „BESPLATNO”. Da je programer napisao cijena = 0 izvan if (greškom uvukao red manje), ovaj test bi i dalje prolazio, a svi kupci bi dobili besplatnu dostavu.
Zato postoji pokrivenost grana (odluka): svaka odluka mora bar jednom dati DA i bar jednom NE.
\[\text{pokrivenost grana} = \frac{\text{izvršeni ishodi odluka}}{2 \times \text{broj odluka}} \times 100\,\%\]
Za cijena_dostave imamo 3 odluke, dakle 6 ishoda:
| Odluka | DA | NE |
|---|---|---|
| red 6: izvan zone? | P1 (−1 km) | N-1001, N-1002 |
| red 8: ispod 5,00 €? | P2 (4,50 €) | N-1001, N-1002 |
| red 10: od 30,00 €? | N-1002 | N-1001 |
Sam N-1001 daje 3 od 6 ishoda (sve NE), a s N-1002 je 4 od 6. Tek s testovima −1 km i 4,50 € imamo sve ishode.
Pokrivenost grana uključuje pokrivenost iskaza: ako je svaka grana izvršena, izvršen je i svaki iskaz na njoj. Obrnuto ne važi, kako pokazuje dostava_s_kodom.
Za radoznale. coverage.py u režimu
--branchispiše jedan broj koji spaja oba kriterija: (izvršeni iskazi + izvršene grane) / (svi iskazi + sve grane). Za N-1001 sam: (7 + 3) / (10 + 6) = 10 / 16 = 62,5 %, a alat ispiše 62 %. Za N-1001 i N-1002: (8 + 4) / 16 = 75 %.
Ukratko.
- Pokrivenost grana traži da svaka odluka bar jednom da DA i bar jednom NE.
ifbezelsemože imati 100 % pokrivenost iskaza, a da grana NE nikad nije izvršena.- Pokrivenost grana uključuje pokrivenost iskaza, ali ne i obrnuto.
- Funkcija
cijena_dostaveima 6 ishoda odluka; N-1001 i N-1002 zajedno pokriju 4.
6.6 Pokrivenost uslova i MC/DC
Pogledajmo pažljivije prvu odluku:
if udaljenost_km < 0 or udaljenost_km > 10:Ona ima dva uslova: A = udaljenost_km < 0 i B = udaljenost_km > 10. Pokrivenost grana traži samo da cijela odluka bude jednom DA i jednom NE. To postižu testovi −1 km (DA) i 5 km (NE). Ali uslov B tada nikad nije bio istinit! Da je programer napisao udaljenost_km > 100 umjesto > 10, ova dva testa ne bi primijetila: 11 km bi se dostavljalo.
Pokrivenost uslova traži da svaki uslov bude bar jednom istinit i bar jednom lažan. To je jače, ali ima zamku: testovi −1 km (A tačno) i 11 km (B tačno) zajedno pokrivaju oba uslova u oba ishoda, ali odluka je u oba slučaja DA. Grana NE uopšte nije izvršena.
Za radoznale. Python računa
orkratko (short-circuit): ako je A tačno, B se uopšte ne računa, jer je odluka već DA. Za −1 km Python nikad ne provjeri-1 > 10. Kad govorimo o vrijednosti uslova B u tom testu, mislimo na vrijednost koju bi imao da je izračunat (lažno).
Rješenje koje se koristi u najstrožim oblastima je MC/DC (Modified Condition/Decision Coverage, izmijenjena pokrivenost uslova i odluka). Pravilo zvuči komplikovano, a ideja je jednostavna:
- svaka odluka bar jednom DA i bar jednom NE (kao grane);
- svaki uslov bar jednom tačan i bar jednom lažan (kao uslovi);
- svaki uslov mora bar jednom sam promijeniti ishod odluke, dok ostali uslovi stoje isto.
Treća tačka je srž. Ona dokazuje da svaki uslov zaista nešto radi. Uslov koji nikad sam ne mijenja ishod mogao bi se obrisati, i niko ne bi primijetio.
Za odluku A or B tražimo par testova za A i par za B:
| Test | km | A: km < 0 | B: km > 10 | Odluka | Pravilo |
|---|---|---|---|---|---|
| T1 | 5 | ne | ne | NE (dostavlja se) | PP-3 |
| T2 | −1 | da | ne | DA (odbijeno) | PP-3 |
| T3 | 11 | ne | da | DA (odbijeno) | PP-3 |
- Par T1 i T2 pokazuje uslov A: B stoji (ne), A se promijeni s ne na da, i odluka se promijeni s NE na DA.
- Par T1 i T3 pokazuje uslov B: A stoji (ne), B se promijeni, i odluka se promijeni.
Tri testa za dva uslova. Opšte pravilo: za odluku s n uslova MC/DC obično treba n + 1 testova. Puna kombinatorika, koja se zove pokrivenost višestrukih uslova, traži sve kombinacije, 2ⁿ: za 2 uslova to su 4, za 10 uslova 1024 testa. Kod nas je četvrta kombinacija (A i B tačni u isto vrijeme: km < 0 i km > 10) nemoguća, pa je MC/DC ovdje isto što i sve moguće kombinacije.
Gdje se MC/DC koristi? Avionski standard DO-178C traži MC/DC za softver najvišeg nivoa kritičnosti (nivo A), čiji otkaz može srušiti avion. Slične zahtjeve imaju standardi za automobile i željeznicu. U web aplikaciji za hranu MC/DC se rijetko mjeri formalno, ali način razmišljanja je koristan svuda: za svaki uslov u složenom if zapitajte se „koji test pokazuje da je ovaj uslov potreban?”
Česta zabluda. „MC/DC je samo za avione.” Mjeri se formalno uglavnom tamo, ali greška „pogrešna granica u jednom od dva uslova” (11 km se dostavlja) je obična greška u svakoj aplikaciji, i MC/DC je hvata s tri testa.
Ukratko.
- Uslov je najmanji logički dio odluke; odluka
km < 0 or km > 10ima dva uslova.- Pokrivenost grana ne traži da svaki uslov bude tačan, a pokrivenost uslova ne traži oba ishoda odluke.
- MC/DC traži da svaki uslov bar jednom sam promijeni ishod odluke, što dokazuje da je potreban.
- Za odluku s n uslova MC/DC obično treba n + 1 testova, a sve kombinacije 2ⁿ.
- Avionski standard DO-178C traži MC/DC za najkritičniji softver.
6.7 Pokrivenost putanja i hijerarhija kriterija
Najjači kriterij je pokrivenost putanja: svaki mogući put od ulaza do izlaza mora biti izvršen. U cijena_dostave postoje samo četiri puta (svaki raise i return završava funkciju), pa su putanje isto što i bazni putevi. To je izuzetak.
Čim funkcija ima petlju, broj putanja eksplodira. Zamislite petlju koja prolazi kroz stavke korpe, a u svakoj stavci ima jedan if (npr. „da li je artikal na akciji”). Za korpu od 1 stavke postoje 2 puta, za 2 stavke 4, za 3 stavke 8, za 20 stavki 2²⁰ = 1 048 576 puteva. Za korpu bez gornje granice puteva je beskonačno mnogo. Pokrivenost putanja je zato teorijski ideal, a u praksi se koriste bazni putevi (V(G) testova) i petlje se testuju s 0, 1, 2 i „mnogo” prolaza.
Kriteriji se mogu poredati po snazi (slika 6.3). Strelica znači „uključuje”: ko ispuni gornji kriterij, ispunio je i donji.

| Kriterij | Šta traži | Testova za cijena_dostave |
|---|---|---|
| Iskazi | svaki red bar jednom | 4 (svaki raise i return je poseban red) |
| Grane | svaka odluka DA i NE | 4 |
| MC/DC | svaki uslov sam mijenja odluku | 5 (4 bazna + 11 km za uslov B) |
| Višestruki uslovi | sve moguće kombinacije uslova | 5 (kombinacija „km < 0 i km > 10” je nemoguća) |
| Putanje | svaki put od ulaza do izlaza | 4 |
Primijetite red za MC/DC: četiri bazna puta testiraju zonu samo sa −1 km, pa uslov B nikad nije tačan. Peti test, 11 km, je upravo onaj koji test_svi.py iz praktične nastave ima.
Na času. Koliko putanja ima funkcija koja ima tri
ifjedan ispod drugog, ali bezreturniraiseunutar njih (svakiifsamo promijeni neku promjenljivu)? Koliki je V(G)? – Putanja je 2 × 2 × 2 = 8, a V(G) = 3 + 1 = 4. Bazni putevi su manje od svih putanja: zato je V(G) praktičan kompromis.
Ukratko.
- Pokrivenost putanja traži svaki mogući put, ali petlje čine broj puteva ogromnim ili beskonačnim.
- U praksi se koriste bazni putevi, a petlje se testiraju s 0, 1, 2 i mnogo prolaza.
- Kriteriji se poredaju po snazi: iskazi, grane, MC/DC, višestruki uslovi.
- Jači kriterij traži više testova, ali daje veće povjerenje.
Provjeri se.
- Može li funkcija imati 100 % pokrivenost iskaza i 50 % grana? – Da:
ifbezelsetestiran samo s ishodom DA.- Koliko testova treba za MC/DC odluke
A or B? – Tri: oba lažna, samo A tačan, samo B tačan.- Zašto se pokrivenost putanja rijetko traži u praksi? – Jer petlje čine broj putanja ogromnim ili beskonačnim.
Dio 3 – Mjerimo: coverage.py
6.8 Prvo mjerenje
Ručno brojanje je dobro za učenje, a za stvarni projekat treba alat. Za Python je to coverage.py. On pokrene testove i za svaki red zapiše da li je izvršen. Instalacija i pokretanje iz foldera primjeri/pokrivenost:
pip install coverage
coverage run -m pytest test_dva.py
coverage report -m
Prva naredba pokreće pytest „pod nadzorom”, druga ispisuje izvještaj (-m znači: pokaži brojeve redova koji nedostaju). Za dva testa iz test_dva.py (N-1001 i N-1002) izvještaj je:
Name Stmts Miss Cover Missing
------------------------------------------
dostava.py 10 2 80% 7, 9
------------------------------------------
TOTAL 10 2 80%
Čitamo red po red: Stmts je broj iskaza (10), Miss broj neizvršenih (2), Cover procenat (80 %), a Missing tačno koji redovi nisu izvršeni: 7 i 9, dva raise. To se slaže s našim ručnim računom iz 6.4.
Dodajemo mjerenje grana opcijom --branch:
coverage run --branch -m pytest test_dva.py
coverage report -m
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------
dostava.py 10 2 6 2 75% 7, 9
--------------------------------------------------------
TOTAL 10 2 6 2 75%
Nove kolone: Branch je broj ishoda odluka (6, kao u tabeli iz 6.5), a BrPart (partial) broj odluka kod kojih je izvršen samo jedan ishod (2: odluke u redovima 6 i 8 nikad nisu dale DA). Ukupno je 75 %.
Za čitljiviji prikaz postoji HTML izvještaj:
coverage html
Otvorite htmlcov/index.html u pregledniku i kliknite na dostava.py. Svaki red je obojen: zeleno izvršen, crveno nije izvršen, žuto odluka kod koje je izvršen samo jedan ishod. Pređite mišem preko žutog reda i alat napiše koja grana fali, npr. „line 6 didn’t jump to line 7”. To je mapa grada s obojenim ulicama iz 6.1.
Za radoznale. Mnogi projekti koriste dodatak pytest-cov, koji isto radi jednom naredbom:
pytest --cov=. --cov-branch --cov-report=term-missing. Rezultat je isti, jer pytest-cov iznutra koristi coverage.py.
Ukratko.
- coverage.py pokreće testove naredbom
coverage run -m pytesti bilježi izvršene redove.coverage report -mispisuje procenat i brojeve redova koji nisu izvršeni.- Opcija
--branchdodaje pokrivenost grana; kolona BrPart broji odluke s izvršenim samo jednim ishodom.coverage htmlpravi izvještaj u kojem su redovi obojeni zeleno, crveno i žuto.
6.9 Od 62 % do 100 %
Kolona Missing nije ocjena, nego spisak zadataka: svaki broj u njoj je test koji fali. Pratimo kako pokrivenost raste dok dodajemo testove (slika 6.4).
Korak 1 – samo N-1001. Pokrivenost grana je 62 %, a nedostaju redovi 7, 9, 11. Treba nam narudžba sa besplatnom dostavom (red 11), nešto izvan zone (red 7) i nešto ispod minimuma (red 9).
Korak 2 – dodajemo N-1002. Red 11 je izvršen; pokrivenost je 75 %, nedostaju 7 i 9.
Korak 3 – dodajemo tri testa izuzetaka iz test_svi.py:
def test_negativna_udaljenost():
with pytest.raises(ValueError):
cijena_dostave(-1, 1000)
def test_izvan_zone():
with pytest.raises(ValueError):
cijena_dostave(11, 1000)
def test_ispod_minimuma():
with pytest.raises(ValueError):
cijena_dostave(5, 450)coverage run --branch -m pytest test_svi.py
coverage report -m
Name Stmts Miss Branch BrPart Cover
----------------------------------------------
dostava.py 10 0 6 0 100%
----------------------------------------------
TOTAL 10 0 6 0 100%
Pet testova, 100 % iskaza i grana. Kolona Missing je prazna.

Primijetite da je test s 11 km tu zbog MC/DC, a ne zbog grana: grane bi bile 100 % i bez njega, jer −1 km već daje DA u prvoj odluci. Bez tog testa bismo imali 100 % grana, a greška > 100 umjesto > 10 bi prošla neopaženo.
Isto mjerenje nad testovima DeliveryApp (primjeri/deliveryapp, coverage run --branch -m pytest tests) daje 98 % za deliveryapp.py. (U folderu su već i testovi iz poglavlja 8; samo s testovima iz poglavlja 4 i 5 rezultat je isti.) Jedini neizvršen red je funkcija eur, koja samo formatira iznos za ekran: za nju još niko nije napisao test.
Na času. U HTML izvještaju jedna linija je žuta:
if iznos >= 3000:s porukom „line 10 didn’t jump to line 11”. Kakav test treba dopisati? – Test u kojem je iznos 30,00 € ili više, npr. N-1002, očekivano 0,00 € dostave.
Ukratko.
- Svaki broj u koloni Missing je zadatak: test koji treba dopisati.
- Za
cijena_dostavepokrivenost grana raste s 62 % (1 test) na 75 % (2 testa) i 100 % (5 testova).- Test s 11 km ne povećava pokrivenost grana, ali ga traži MC/DC.
- Testovi DeliveryApp iz poglavlja 4 i 5 pokrivaju 98 % fajla
deliveryapp.py.
6.10 Zašto 100 % nije kraj
Imamo 100 % iskaza i grana. Da li to znači da je cijena_dostave bez grešaka? Napravimo eksperiment. U dostava.py promijenite jedan znak, kao u grešci D-01 iz prve verzije DeliveryApp:
if iznos > 3000: # bilo je >= 3000
return 0Sada narudžba od tačno 30,00 € plaća dostavu, što krši PP-4. Pokrenimo isti skup testova:
coverage run --branch -m pytest test_svi.py
..... [100%]
5 passed in 0.01s
Name Stmts Miss Branch BrPart Cover
----------------------------------------------
dostava.py 10 0 6 0 100%
Pet prolaza i 100 % pokrivenosti, a kod je pogrešan. Zašto? Pokrivenost mjeri da li je red izvršen, a ne da li je provjeren pravom vrijednošću. Naš test za besplatnu dostavu koristi 32,00 €, a za 32,00 € su i >= i > tačni. Nijedan test ne stoji na granici.
Grešku hvata test iz test_granica.py, napravljen iz pravila, a ne iz koda:
def test_tacno_30_eura_besplatno():
assert cijena_dostave(5, 3000) == 0
def test_29_99_placa_dostavu():
assert cijena_dostave(5, 2999) == 300FAILED test_granica.py::test_tacno_30_eura_besplatno - assert 300 == 0
1 failed, 6 passed in 0.04s
Za tačno 30,00 € pogrešan kod vraća 300 centi (3,00 € za 5 km), a pravilo kaže 0. Ovakve testove pravi analiza graničnih vrijednosti, tehnika crne kutije iz poglavlja 8. A ideja „namjerno pokvari kod i vidi da li testovi to primijete” zove se testiranje mutacijama i učimo je u poglavlju 7.
Tri pouke za pamćenje:
- Pokrivenost pokazuje šta NIJE testirano, i tu je vrlo korisna. Crveni red je sigurno netestiran.
- Pokrivenost ne pokazuje da je nešto DOBRO testirano. Zeleni red je izvršen, ali možda bez provjere (test bez
asserttakođer podiže pokrivenost!) ili s pogrešnim podacima. - Kad pokrivenost postane cilj, prestaje biti dobra mjera. Tim kojem šef kaže „mora biti 100 %” brzo nauči pisati testove koji izvršavaju kod, ali ništa ne provjeravaju.
Zato se u praksi postavlja razuman prag, a ne 100 %. coverage.py ga može provjeriti sam:
coverage report --fail-under=80
Ako je pokrivenost ispod 80 %, naredba završava greškom (za test_dva.py: „Coverage failure: total of 75 is less than fail-under=80”). U poglavlju 14 tu naredbu stavljamo u GitHub Actions, pa svaki push koji spusti pokrivenost ispod praga dobije crveni znak.
Česta zabluda. „Imamo 100 % pokrivenosti, dakle nemamo grešaka.” Ne: imamo dokaz da je svaki red izvršen, ne da je svaki red tačan. D-01 je preživio 100 % pokrivenosti.
Ukratko.
- Pokrivenost mjeri da li je kod izvršen, a ne da li je rezultat provjeren pravom vrijednošću.
- Greška D-01 (
>umjesto>=) preživi 100 % pokrivenosti, a uhvati je test s tačno 30,00 €.- Pokrivenost je odlična za nalaženje netestiranog koda, a slaba kao dokaz kvaliteta testova.
- U praksi se postavlja razuman prag, npr.
--fail-under=80, a ne slijepa trka za 100 %.
Provjeri se.
- Šta znači kolona Missing u izvještaju
coverage report -m? – Brojevi redova koje nijedan test nije izvršio.- Da li test bez
assertpovećava pokrivenost? – Da: kod je izvršen, iako ništa nije provjereno.- Koji test hvata D-01 kad 100 % pokrivenosti ne hvata? – Test na granici: tačno 30,00 € mora dati besplatnu dostavu.
Praktična nastava – Mjerimo pokrivenost DeliveryApp
Cilj je izmjeriti pokrivenost koda DeliveryApp, pročitati izvještaj, dopisati testove dok pokrivenost grana ne bude 100 % i pokazati da 100 % ne hvata sve greške. Radi se u parovima.
Korak 1 – Graf i bazni putevi rukom (30 minuta)
- Nacrtajte na papiru graf toka kontrole funkcije
napojnica(iznos, procenat)izprimjeri/deliveryapp/napojnica.pyiz poglavlja 5. - Izračunajte V(G) na dva načina (E − N + 2 i broj odluka + 1).
- Napišite tabelu baznih puteva: put, ulaz, očekivani rezultat, pravilo (PP-8). Očekivani rezultat računajte iz pravila, ne iz koda.
Korak 2 – Od 62 % do 100 % (45 minuta)
- U folderu
primjeri/pokrivenostinstalirajte coverage (pip install coverage) i pokrenitecoverage run --branch -m pytest test_dva.pyicoverage report -m. Zapišite brojeve. - Napravite svoj fajl
test_moji.pykoji počinje samo s testom N-1001. Izmjerite (očekivano 62 %). - Dodajte po jedan test, svaki put izmjerite i zapišite tabelu: test, pokrivenost iskaza, pokrivenost grana, redovi u Missing.
- Otvorite
coverage htmli napravite snimak ekrana prije i poslije (crveni, žuti i zeleni redovi).
Korak 3 – Pokrivenost cijelog DeliveryApp (30 minuta)
- U folderu
primjeri/deliveryapppokrenitecoverage run --branch -m pytest testsicoverage report -m --include="deliveryapp.py,napojnica.py". - Koji red nije pokriven? Dopišite test za funkciju
eur(npr. 2450 → „24,50 €”, 5 → „0,05 €”) i doveditedeliveryapp.pydo 100 %. - Pokrenite isto mjerenje sa
DeliveryApp=v0(u PowerShellu$env:DeliveryApp="v0") icoverage report -mbez opcije--include. Pokrivenost se mjeri i kad testovi padaju: 10 testova pada, adeliveryapp_v0.pyje ipak pokriven 98 %. Zapišite šta to govori o odnosu „pokrivenost” i „prolaz”.
Korak 4 – Mutant koji preživi 100 % (30 minuta)
- Dok
test_svi.pydaje 100 %, udostava.pypromijenite>=u>. Pokrenite testove: šta se desi s pokrivenošću i s prolazima? - Napravite još tri male izmjene, jednu po jednu:
udaljenost_km > 10u> 100;200 + 50u200 + 60;math.ceil(max(0, udaljenost_km - 3))umath.ceil(udaljenost_km - 3)(bezmax). Za svaku zapišite: da litest_svi.pypada? (Savjet za treću: koliko jemath.ceil(1 - 3)?) - Za svaku izmjenu koja preživi napišite test koji je hvata (kao
test_granica.py). Na kraju vratite originalni kod i provjerite da svi testovi prolaze.
Korak 5 – Prag pokrivenosti (15 minuta)
- Pokrenite
coverage report --fail-under=80satest_dva.pyi satest_svi.py. Zapišite poruke i kod izlaza (u PowerShellu$LASTEXITCODE). - Raspravite u paru: koji prag biste postavili za DeliveryApp i zašto ne 100 %?
Korak 6 – AI i pokrivenost (opcionalno)
Dajte AI asistentu funkciju cijena_dostave (bez testova) i zatražite „testove za 100 % pokrivenosti grana”. Pokrenite ih sa --branch. Da li su 100 %? Da li bi uhvatili > umjesto >=? Zapišite koliko testova ima očekivani rezultat izračunat iz pravila, a koliko je samo prepisalo ono što kod vrati.
Rezultat koji se predaje: fajl rezultati-6.md s grafom funkcije napojnica (slika ili Mermaid), V(G) i tabelom baznih puteva; tabela rasta pokrivenosti iz koraka 2 sa snimcima HTML izvještaja; test za eur i izvještaj sa 100 %; tabela mutanata iz koraka 4 s testovima koji ih hvataju; odgovor o pragu iz koraka 5.
Vježbe
Vježba za studenta 1 – graf i V(G).
Nacrtajte graf toka kontrole i izračunajte V(G) za funkciju:
def moze_otkazati(stanje):
if stanje == "NOVA":
return True
if stanje == "PRIHVAĆENA":
return True
return FalseRješenje. Čvorovi: odluka 1 (stanje == "NOVA"), return True (2), odluka 3, return True (4), return False (5), kraj (6): 6 čvorova. Grane: 1→2, 1→3, 3→4, 3→5, 2→kraj, 4→kraj, 5→kraj: 7 grana. V(G) = 7 − 6 + 2 = 3 = 2 odluke + 1. Bazni putevi: „NOVA” → True; „PRIHVAĆENA” → True; „U_PRIPREMI” → False (PP-6).
Vježba za studenta 2 – iskazi i grane.
Za funkciju dostava_s_kodom iz 6.5 imamo jedan test: dostava_s_kodom(300, "BESPLATNO") == 0. Kolika je pokrivenost iskaza, a kolika grana? Koji test dodati?
Rješenje. Iskazi: 100 % (sva četiri). Grane: odluka je dala samo DA, pa je pokrivena 1 od 2 grane. coverage.py ispiše 83 % uz napomenu 5->7: grana iz reda 5 direktno u red 7 (ishod NE) nikad nije izvršena. Dodati test dostava_s_kodom(300, "") == 300 (ili s bilo kojim drugim kodom): bez koda cijena ostaje 3,00 €.
Vježba za studenta 3 – MC/DC.
Odluka glasi if iznos >= 3000 and kod_ok: (besplatna dostava samo s važećim kodom). Napravite najmanji skup testova za MC/DC.
Rješenje. Za A and B tri testa: (da, da) → DA; (ne, da) → NE (pokazuje A); (da, ne) → NE (pokazuje B). Npr. 32,00 € i važeći kod → besplatno; 29,99 € i važeći kod → plaća; 32,00 € i nevažeći kod → plaća. Primijetite: za and polazni test je „oba tačna”, a za or (6.6) „oba lažna”.
Vježba za studenta 4 – čitanje izvještaja.
Izvještaj glasi:
Name Stmts Miss Branch BrPart Cover Missing
--------------------------------------------------------
dostava.py 10 3 6 3 62% 7, 9, 11
Šta testovi rade, a šta ne? Koja tri testa dopisati?
Rješenje. Izvršena je samo plaćena dostava (sve tri odluke daju NE); to je jedan test kao N-1001. Fale: red 7 (izvan zone, npr. −1 km), red 9 (ispod minimuma, npr. 4,50 €) i red 11 (besplatna dostava, npr. 32,00 €). Uz test 11 km zbog MC/DC dobijamo test_svi.py.
Vježba za studenta 5 – 100 % i greška.
Kolega kaže: „Funkcija popust ima 100 % pokrivenosti grana, ne treba joj više testova.” Navedite grešku koja bi mogla proći.
Rješenje. Npr. MAX_POPUST = 800 umjesto 500 (greška slična D-05): ako nijedan test nema iznos veći od 50,00 €, min uvijek vrati izračunati popust i pokrivenost ostaje 100 %, a greška prolazi. Treba test granice: 80,00 € sa STUDENT10 → 5,00 €. Druga mogućnost: kod malim slovima „student10” (D-03) – grana je ista, ali ulaz drugačiji.
Sažetak u pet rečenica
- Greška goto fail iz 2014. bila je jedan ponovljen red koji je preskakao provjeru potpisa, a mjerenje pokrivenosti bi pokazalo da se ti redovi nikad ne izvrše.
- Bijela kutija pravi i ocjenjuje testove gledajući kod, crta ga kao graf toka kontrole i broji nezavisne puteve ciklomatskom složenošću V(G) = broj odluka + 1, što je za
cijena_dostave4. - Kriteriji pokrivenosti idu od slabijeg ka jačem: iskazi, grane, MC/DC i višestruki uslovi, a pokrivenost putanja je zbog petlji uglavnom teorijski ideal.
- Alat coverage.py mjeri pokrivenost naredbama
coverage run --branch -m pytesticoverage report -m, a kolona Missing je spisak testova koje treba dopisati, od 62 % do 100 %. - Pokrivenost pokazuje šta nije testirano, ali ne i da je testirano dobro: greška D-01 preživljava 100 % pokrivenosti, a hvata je tek test na granici od 30,00 €.