Dio 1 – Tok podataka
Dio 2 – Statička analiza
Dio 3 – Testiranje mutacijama
Praksa – Tok podataka, alati i mutanti
Materijale priredio Erkan Islamović
erkan.islamovic@eubd.edu.ba · erkan.islamovic@gmail.com
Petnaest sedmica: svaka ima tri časa teorije i tri časa praktične nastave. Danas: poglavlje 7 (Sedmica 7, označeno). Isti primjer kroz knjigu: DeliveryApp, narudžba N-1001.
| Sed. | Poglavlje | Teorija (3 časa) | Praksa (3 časa) |
|---|---|---|---|
| 1 | Zašto testiramo | Uvodne priče; greška, defekt, otkaz; sedam principa; DeliveryApp | Tabela očekivanih cijena rukom |
| 2 | Proces i nivoi | Test proces; nivoi i V-model; vrste; statičko testiranje | Recenzija zahtjeva DeliveryApp |
| 3 | Manuelno testiranje | Test slučaj; izvještaj o defektu; istraživačko testiranje | Test slučajevi za web stranicu |
| 4 | Automatizovano testiranje | Šta automatizovati; piramida testova; prvi test | Prvi pytest |
| 5 | Jedinično testiranje | AAA; fixture, parametri, izuzeci; TDD | Testovi za dostavu i popust |
| 6 | Bijela kutija | Graf toka; pokrivenost; ciklomatska složenost | coverage.py do 100 % grana |
| 7 | Tok podataka | Definicija i upotreba; anomalije; statička analiza, mutacije | ruff, mypy, ručna mutacija |
| 8 | Crna kutija | Ekvivalencija i granice; tabela odlučivanja; stanja | Lov na pet grešaka |
| Sed. | Poglavlje | Teorija (3 časa) | Praksa (3 časa) |
|---|---|---|---|
| 9 | Model i integracija | Model; strategije integracije; API | TestClient za API |
| 10 | Sistemsko testiranje | Sistemsko; performanse i sigurnost; prihvatno i BDD | Locust, Gherkin |
| 11 | OO testiranje | Stanje objekta; nasljeđivanje; dvojnici | Narudzba, unittest.mock |
| 12 | Paralelne aplikacije | Trka podataka; zastoj; nestabilni testovi | Niti, 1000 ponavljanja |
| 13 | Web aplikacije | Web i DOM; Playwright; Page Object, pristupačnost | Playwright za DeliveryApp |
| 14 | Upravljanje testiranjem | Test plan i rizik; metrike; CI/CD | GitHub Actions |
| 15 | Alati, AI, projekat | Pregled alata; AI u testiranju; ISTQB | Odbrana projekta |
Dok koristite internet, vaš računar i server stalno provjeravaju da li je onaj drugi još „živ”. U biblioteci OpenSSL, koja je 2014. čuvala veliki dio sigurnih (HTTPS) veza na svijetu, ta provjera se zvala heartbeat, „otkucaj srca”. Klijent pošalje kratku poruku, recimo riječ „PTICA”, i kaže: „Ako si tu, vrati mi ovu riječ; ima 5 slova.” Server vrati „PTICA”. Ali šta ako klijent slaže i kaže: „Vrati mi PTICA, ima 500 slova”? Server je vjerovao broju koji je dobio i nije provjeravao da li je riječ zaista toliko duga. Zato je vratio 5 slova riječi i još 495 bajtova onoga što je slučajno stajalo pored nje u memoriji. A tamo su mogle biti lozinke drugih korisnika, brojevi kartica, pa i tajni ključ samog servera.
Napadač je mogao ponavljati zahtjev i svaki put dobiti do 64 KB memorije, bez ikakvog traga u zapisima servera. Grešku su nezavisno otkrili Neel Mehta iz Googlea i inženjeri finske firme Codenomicon, a objavljena je 7. aprila 2014. pod oznakom CVE-2014-0160 i imenom Heartbleed, „srce koje krvari”. Procjene su govorile da je u tom trenutku ranjivo bilo oko 17 % sigurnih web servera na internetu, oko pola miliona. Cijela greška je bila jedna provjera koja nedostaje: broj koji je došao spolja korišten je bez provjere. Kasnije se pokazalo i nešto važno za testere: poznati komercijalni alat za statičku analizu Coverity nije prijavio grešku, sve dok njegovi autori nisu dodali novo pravilo upravo za ovakve slučajeve. Ovo poglavlje govori o tri načina da se uhvate takve greške: praćenjem toka podataka, statičkom analizom i testiranjem mutacijama.

U poglavlju 6 gledali smo kod kao mapu puteva: graf toka kontrole, grane, pokrivenost. Ostala su dva pitanja. Prvo: šta se dešava s podacima koji putuju tim putevima? Promjenljiva može biti upotrijebljena prije nego što je dobila vrijednost, ili dobiti vrijednost koju niko nikad ne pročita. Drugo: kako znamo da su testovi dobri? Vidjeli smo da greška D-01 preživi 100 % pokrivenosti. Ovo poglavlje odgovara na oba pitanja. Prvo učimo tok podataka i njegove anomalije. Zatim statičku analizu: alate ruff i mypy koji čitaju kod bez pokretanja. Na kraju testiranje mutacijama: namjerno kvarimo kod da bismo vidjeli da li ga testovi čuvaju.
dostava = 300.ukupno = iznos + dostava) ili u odluci (if dostava > 0).>= umjesto >.Nakon poglavlja možete:
za svaki red funkcije označiti definicije i upotrebe promjenljivih; prepoznati tri anomalije toka podataka i objasniti zašto su opasne; nabrojati parove definicija–upotreba i napraviti testove koji ih pokrivaju; objasniti šta je statička analiza i šta može, a šta ne može naći; pokrenuti ruff i mypy i pročitati njihove poruke; razlikovati lažnu uzbunu i propust; objasniti ideju testiranja mutacijama; izračunati rezultat mutacija; za preživjelog mutanta napisati test koji ga ubija; objasniti zašto su mnogi preživjeli mutanti na granicama.
| Čas | Sadržaj | Na času |
|---|---|---|
| Teorija 1 | 7.1–7.3 Definicija i upotreba; anomalije; kriteriji toka podataka | Šta nije u redu s funkcijom racun? |
| Teorija 2 | 7.4–7.6 Statička analiza; ruff; mypy i tipovi | Koji alat hvata koju anomaliju |
| Teorija 3 | 7.7–7.9 Mutacije: ideja, rezultat, ekvivalentni mutanti | 100 % pokrivenosti, 58 % mutacija |
Svaka promjenljiva ima mali „život”. Rodi se kad dobije vrijednost (definicija), koristi se kad se ta vrijednost pročita (upotreba), a nestaje kad funkcija završi. Slika iz života: paket na pošti. Neko ga pošalje (definicija), neko ga preuzme (upotreba). Problem je ako neko dođe po paket koji niko nije poslao, ako se pošalju dva paketa zaredom pa prvi završi u smeću, ili ako paket stoji na pošti i niko ga nikad ne preuzme.
U kodu to izgleda ovako:
dostava = 200 + 50 * dodatni_km # d: dostava je definisana
if iznos >= 3000: # u: iznos je upotrijebljen u odluci
dostava = 0 # d: dostava je ponovo definisana
ukupno = iznos - p + dostava # u: iznos, p i dostava upotrijebljeni u računu; d: ukupno
return ukupno # u: ukupnoUpotrebe se ponekad dijele na dvije vrste:
ukupno = iznos + dostava;if iznos >= 3000.Zašto se testera tiče gdje je promjenljiva rođena i gdje je pročitana? Zato što je mnogo grešaka upravo u tom putovanju: vrijednost nije stigla tamo gdje treba, stigla je pogrešna vrijednost (prepisana u međuvremenu), ili je izračunata pa zaboravljena. Heartbleed je primjer prve vrste: broj „500” je stigao spolja i otišao pravo u kopiranje memorije, bez provjere usput.
if ili while).Postoje tri sumnjiva redoslijeda, i svaki ima kratku oznaku od dva slova:
| Anomalija | Šta znači | Zašto je opasna |
|---|---|---|
| u bez d (ur, upotreba prije definicije) | vrijednost se čita, a nikad nije dodijeljena | u Pythonu greška pri izvršavanju; u C-u slučajna vrijednost iz memorije |
| dd (dvije definicije zaredom) | vrijednost se dodijeli, pa se prepiše prije nego što je iko pročita | prva vrijednost je uzalud izračunata, a često to znači da je zaboravljen uslov |
| d bez u (du, definicija bez upotrebe) | vrijednost se izračuna i nikad ne pročita | zaboravljen dio računa: nešto je trebalo ući u rezultat, a nije |
Pogledajmo funkciju iz primjeri/tok_podataka/racun.py. Kolega je trebao napisati račun s popustom i napojnicom, ali je pisao u žurbi:
import math
import os
def racun(iznos: int, km: float, kod: str = "", procenat: int = 0) -> int:
"""Ukupno za naplatu u centima: artikli − popust + dostava + napojnica."""
if kod:
p = min(iznos // 10, 500)
dostava = 200 + 50 * math.ceil(max(0, km - 3))
dostava = 0
napojnica = iznos * procenat // 100
ukupno = iznos - p + dostava
return ukupnoOznačimo definicije i upotrebe red po red (slika 7.2):

Tri anomalije, tri greške:
p. Ako kupac nema kod, red p = … se preskoči, a p se ipak koristi u računu. Python javlja grešku pri izvršavanju.dostava. Prva vrijednost (cijena po kilometrima) odmah je prepisana nulom. Programer je vjerovatno htio „ako je iznos 30,00 € ili više, dostava je 0”, ali je zaboravio if. Rezultat: svi dobijaju besplatnu dostavu.napojnicu. Napojnica je izračunata i nikad dodata u ukupno. Dostavljač ostaje bez napojnice.Usput, i import os je definicija bez upotrebe: modul je uvezen, a nikad korišten. To nije opasno, ali je nered.
Šta kažu testovi iz primjeri/tok_podataka/test_racun.py? Svaki ima očekivani rezultat iz pravila:
| Test | Ulaz | Očekivano | Pravilo |
|---|---|---|---|
test_n1001_bez_koda | 21,50 €, 4,2 km | 24,50 € | PP-2 |
test_n1001_student10 | 21,50 €, 4,2 km, STUDENT10 | 22,35 € | PP-2, PP-5 |
test_n1001_napojnica_10_posto | 21,50 €, 4,2 km, 10 % | 26,70 € (napojnica 2,20 €) | PP-2, PP-8 |
test_n1002_besplatna_dostava | 32,00 €, 2,5 km | 32,00 € | PP-4 |
pytest -q test_racun.py
FAILED test_racun.py::test_n1001_bez_koda - UnboundLocalError: cannot access ...
FAILED test_racun.py::test_n1001_student10 - AssertionError: assert 1935 == 2235
FAILED test_racun.py::test_n1001_napojnica_10_posto - UnboundLocalError: cann...
FAILED test_racun.py::test_n1002_besplatna_dostava - UnboundLocalError: canno...
4 failed in 0.04sSva četiri padaju. Puna poruka za p glasi: UnboundLocalError: cannot access local variable 'p' where it is not associated with a value, doslovno „ne mogu pročitati lokalnu promjenljivu p, jer nema vrijednost”. A jedini test s kodom pokazuje 19,35 € umjesto 22,35 €: nedostaju tačno 3,00 € dostave koje je prepisala anomalija dd.
Ako bismo popravili samo anomaliju za p (dodali p = 0 na početak), koji testovi bi i dalje padali i zašto? – Svi osim N-1002: dostava je i dalje uvijek 0 (dd), pa N-1001 daje 21,50 € umjesto 24,50 €, a napojnica se i dalje ne dodaje (d bez u). N-1002 bi slučajno prošao, jer je za 32,00 € dostava stvarno 0.
dostava = 0 bez if.Kao što pokrivenost grana traži da svaka grana bude izvršena, kriteriji toka podataka traže da svako putovanje vrijednosti bude provjereno. Pogledajmo ispravljenu funkciju iz racun_ispravljen.py:
def racun(iznos: int, km: float, kod: str = "", procenat: int = 0) -> int:
"""Ukupno za naplatu u centima: artikli − popust + dostava + napojnica."""
p = 0 # d1(p)
if kod.strip().upper() == "STUDENT10":
p = min(round(iznos * 10 / 100), 500) # d2(p)
dostava = 200 + 50 * math.ceil(max(0, km - 3)) # d1(dostava)
if iznos >= 3000:
dostava = 0 # d2(dostava)
centi = -(-iznos * procenat // 100)
napojnica = min(-(-centi // 10) * 10, 1000)
return iznos - p + dostava + napojnica # u(p), u(dostava), u(napojnica)Sada nema anomalija: svaka promjenljiva je definisana prije upotrebe, i svaka definicija se negdje čita. Ali promjenljive p i dostava imaju po dvije definicije koje stižu do iste upotrebe u return. To daje parove definicija–upotreba:
| Par | Kada vrijednost stiže do return | Test koji ga pokriva |
|---|---|---|
| d1(p) → u(p) | bez važećeg koda: popust 0 | test_n1001_bez_koda |
| d2(p) → u(p) | s kodom STUDENT10 | test_n1001_student10 |
| d1(dostava) → u(dostava) | iznos ispod 30,00 € | test_n1001_bez_koda |
| d2(dostava) → u(dostava) | iznos 30,00 € ili više | test_n1002_besplatna_dostava |
| d(napojnica) → u(napojnica) | uvijek; zanimljivo s procentom većim od 0 | test_n1001_napojnica_10_posto |
Tri najpoznatija kriterija, od slabijeg ka jačem:
Naša četiri testa ispunjavaju kriterij sve upotrebe. Pokrenuti nad ispravljenom verzijom (RACUN=ispravljen, u PowerShellu $env:RACUN="ispravljen"), svi prolaze:
pytest -q test_racun.py
.... [100%]
4 passed in 0.01sPo čemu se ovo razlikuje od pokrivenosti grana? Pokrivenost grana pita „da li je svaki put prođen”, a tok podataka pita „da li je svaka vrijednost stigla tamo gdje se koristi i da li je tamo provjerena”. Kod koji ima malo odluka, a mnogo računa (kao racun), ima malo grana, ali mnogo putovanja vrijednosti, i tu je tok podataka korisniji.
Analiza toka podataka ne služi samo testerima. Kompajleri je koriste stalno: da otkriju promjenljive koje se ne koriste, da preskoče račun čiji se rezultat nikad ne čita, i da upozore na promjenljivu koja „možda nije inicijalizovana”. Upravo to rade i alati iz Dijela 2.
racun pokrivaju svih pet parova i prolaze nad ispravljenom verzijom.x = 5 odmah iza x = 3, bez čitanja x između? Dvije definicije zaredom (dd): prva vrijednost se nikad ne pročita.UnboundLocalError: cannot access local variable 'p'? Promjenljiva p je upotrijebljena prije nego što je dobila vrijednost (u bez d).Odgovori: tipka R ili klik na zamućeno polje.
Testovi iz Dijela 1 su našli sve tri anomalije, ali tek kad smo ih pokrenuli. Postoji i brži put: alat koji čita kod i javlja sumnjiva mjesta, bez pokretanja. To je statička analiza, ista ideja kao statičko testiranje iz poglavlja 2 (recenzija), samo što čita računar.
Slika iz života: provjera pravopisa u Wordu. Ona ne zna da li je vaš seminarski rad pametan, ali za sekundu podvuče svaku riječ koja ne postoji. Isto tako linter ne zna da li je račun tačan po pravilu PP-2, ali odmah vidi promjenljivu koja se nikad ne koristi.
Šta statička analiza može:
Šta ne može:
Zato statička analiza ne zamjenjuje testove, nego ih dopunjuje. Najveća prednost joj je cijena: radi za sekundu, na svakom fajlu, prije nego što je iko napisao ijedan test.
Nakon Heartbleeda firma Coverity, koja pravi poznat alat za statičku analizu, provjerila je da li bi njen alat našao grešku. Odgovor je bio „ne”, čak ni uz razna podešavanja. Tok podataka od mrežne poruke do kopiranja memorije bio je presložen za alat. Inženjeri su zatim dodali novo pravilo: vrijednost koja prolazi kroz zamjenu redoslijeda bajtova (što je tipično za podatke koji stižu s mreže) smatra se „nepouzdanom” dok se ne provjeri. S tim pravilom alat je našao Heartbleed. Pouka: statička analiza je moćna, ali nalazi ono što zna tražiti.
ruff je linter za Python, napisan u jeziku Rust, i zato vrlo brz: cijeli veliki projekat provjeri za manje od sekunde. Objedinjuje pravila starijih alata (pyflakes, pycodestyle, isort, mccabe i mnogih drugih). Instalacija i pokretanje:
pip install ruff
ruff check racun.py --output-format conciseracun.py:6:8: F401 [*] `os` imported but unused
racun.py:15:5: F841 Local variable `napojnica` is assigned to but never used
Found 2 errors.
[*] 1 fixable with `--fix` (1 unsafe fix available with `--fix --unsafe-fixes`).Svaka poruka ima: fajl, red i kolonu (racun.py:15:5), šifru pravila (F841) i opis. Prevod:
os je uvezen, a nije korišten (definicija bez upotrebe na nivou modula);napojnica je dobila vrijednost koja se nikad ne koristi (anomalija d bez u).Zvjezdica [*] znači da ruff može sam ispraviti problem opcijom --fix. Ali oprez: za F841 ruff predlaže da obriše dodjelu. To bi ućutkalo upozorenje, ali greška bi ostala: napojnica i dalje ne bi ulazila u račun. Alat je pokazao simptom, a uzrok mora popraviti čovjek: napojnica treba ući u ukupno.
Šta ruff nije javio? Ni upotrebu p bez definicije, ni dostava prepisanu nulom. Za ruff je dostava = 0 sasvim legalan red. To su dva propusta.
ruff zna i izmjeriti složenost iz poglavlja 6, pravilom C901. Granica se zadaje u fajlu pyproject.toml ili na komandnoj liniji. Za vježbu postavimo je nisko, na 3:
ruff check --isolated --select C901 --config "lint.mccabe.max-complexity=3" ../pokrivenost/dostava.py --output-format concise…\primjeri\pokrivenost\dostava.py:5:5: C901 `cijena_dostave` is too complex (4 > 3)
Found 1 error.(Za fajl izvan trenutnog foldera ruff ispiše punu putanju.) Isti broj 4 koji smo u poglavlju 6 izračunali rukom. U pravom projektu granica je obično 10, u fajlu pyproject.toml:
[tool.ruff.lint]
select = ["E", "F", "B", "C90"]
[tool.ruff.lint.mccabe]
max-complexity = 10(E su pravila stila, F greške kao F401 i F841, B česti „bagovi” iz alata flake8-bugbear, C90 složenost.)
Pokrenut nad cijelim folderom primjeri/deliveryapp sa podrazumijevanim pravilima (ruff check --isolated .), ruff javlja samo jedno: deliveryapp_v0.py:9:8: F401 'math' imported but unused. U ispravnoj verziji math se koristi za math.ceil (zaokruživanje kilometara naviše). Zašto bi prva verzija uvozila math, a ne koristila ga? Zapišite pretpostavku; provjerićemo je u poglavlju 8.
Python ne traži da napišete tip promjenljive. Ali možete ga napisati, kao oznaku tipa (type hint): iznos: int, km: float, -> str. Python sam te oznake ne provjerava, ali alat mypy ih čita i javlja kad se ne slažu.
def eur(centi: int) -> str:
return f"{centi // 100},{centi % 100:02d} €"
if __name__ == "__main__":
print(eur(24.5))Programer je zaboravio da su iznosi u centima (poglavlje 5) i poslao 24.5 umjesto 2450. Pokretanje programa pada tek kad dođe do tog reda:
python racun.py
ValueError: Unknown format code 'd' for object of type 'float'mypy to vidi bez pokretanja:
pip install mypy
mypy --enable-error-code possibly-undefined racun.pyracun.py:16: error: Name "p" may be undefined [possibly-undefined]
racun.py:25: error: Argument 1 to "eur" has incompatible type "float"; expected "int" [arg-type]
Found 2 errors in 1 file (checked 1 source file)Druga poruka je greška tipa: funkcija očekuje int, a dobija float. Prva poruka je naša anomalija u bez d: „ime p možda nije definisano”. Tu provjeru mypy radi samo kad je uključimo opcijom --enable-error-code possibly-undefined; bez nje bi javio samo grešku tipa.
Saberimo šta je ko našao u funkciji racun (slika 7.3):
| Problem | ruff | mypy | testovi |
|---|---|---|---|
import os nekorišten | da (F401) | ne | ne |
napojnica izračunata, nekorištena (d bez u) | da (F841) | ne | da (26,70 €) |
p možda nedefinisan (u bez d) | ne | da (uz opciju) | da (UnboundLocalError) |
dostava prepisana nulom (dd) | ne | ne | da (22,35 €) |
eur(24.5) pogrešan tip | ne | da | ne (kod nije u testovima) |

Nijedan alat nije našao sve. Anomaliju dd nije našao nijedan statički alat, a pogrešan tip nije našao nijedan test. Zato se u ozbiljnim projektima koriste sva sita zajedno, i to automatski, pri svakom push (poglavlje 14).
„Alat nije javio ništa, dakle kod je ispravan.” Ćutanje alata znači samo da alat nije našao ono što zna tražiti. Coverity je ćutao o Heartbleedu.
iznos: int) Python ne provjerava, ali ih mypy provjerava bez pokretanja koda.24.5 umjesto centi, prije nego što je program pao.possibly-undefined mypy javlja i promjenljivu koja možda nije definisana.racun nije našao ni ruff ni mypy? Dvije definicije zaredom (dostava = 0), koju je našao tek test.Odgovori: tipka R ili klik na zamućeno polje.
Testovi čuvaju kod. Ali ko čuva testove? U poglavlju 6 vidjeli smo da pet testova sa 100 % pokrivenosti ne primijeti > umjesto >=. Kako izmjeriti koliko su testovi stvarno jaki?
Ideja testiranja mutacijama je jednostavna i pomalo drska: namjerno pokvarimo kod i pogledamo da li testovi to primijete. Slika iz života: vatrogasna vježba. Ne čekamo pravi požar da vidimo radi li alarm; zapalimo malo dima i gledamo da li zazvoni. Ako ne zazvoni, alarm ne valja, iako je „uključen” (kao što je kod „pokriven”).
Postupak:
>= u >, + u -, 200 u 250, or u and).Izmjene se prave po operatorima mutacije, malim pravilima „šta zamijeniti čime”:
| Operator | Primjer u cijena_dostave | Koju grešku glumi |
|---|---|---|
| zamjena relacionog operatora | >= → >, < → <= | granica pomjerena za jedan (D-01) |
| zamjena logičkog operatora | or → and | pogrešno spojeni uslovi |
| zamjena aritmetičkog operatora | 50 * km → 50 + km | pogrešna formula |
| zamjena konstante | 200 → 250, - 3 → - 4 | pogrešno prepisan broj iz pravila |
| brisanje poziva | max(0, x) → x | zaboravljen slučaj |
| zamjena funkcije | math.ceil → round | pogrešno zaokruživanje |
Testiranje mutacijama se oslanja na dvije pretpostavke. Prva: programeri uglavnom pišu skoro tačan kod, pa su prave greške male, kao mutanti. Druga, efekat sprezanja (coupling effect): testovi koji hvataju male greške obično hvataju i veće, složene greške. Obje su potvrđene u brojnim istraživanjima, iako ne važe uvijek.
cijena_dostaveU folderu primjeri/mutacije je mala skripta mutanti.py. Ona uzme cijena_dostave iz primjeri/pokrivenost/dostava.py, napravi 12 mutanata (po jedan za svaki red tabele ispod), nad svakim pokrene zadate testove i ispiše rezultat. Pokrenimo je s testovima iz poglavlja 6 koji imaju 100 % pokrivenosti:
python mutanti.py ../pokrivenost/test_svi.pycijena_dostaveM1 km < 0 -> km <= 0 PREŽIVIO
M2 km > 10 -> km >= 10 PREŽIVIO
M3 km > 10 -> km > 100 ubijen
M4 or -> and ubijen
M5 iznos < 500 -> iznos <= 500 PREŽIVIO
M6 iznos >= 3000 -> iznos > 3000 PREŽIVIO
M7 return 0 -> return 1 ubijen
M8 bez max(0, …) PREŽIVIO
M9 km − 3 -> km − 4 ubijen
M10 ceil -> round ubijen
M11 200 + -> 250 + ubijen
M12 50 * -> 50 + ubijen
Ubijeno 7 od 12: rezultat mutacija 58 %cijena_dostave100 % pokrivenosti, a samo 58 % mutacija. Pet mutanata je preživjelo. Pogledajmo ih kao detektivi:
cijena_dostave| Mutant | Šta mijenja | Zašto je preživio | Test koji ga ubija |
|---|---|---|---|
| M1 | 0 km se odbija | nijedan test nema tačno 0 km | 0 km, 10,00 € → 2,00 € |
| M2 | 10 km se odbija | nijedan test nema tačno 10 km | 10 km, 10,00 € → 5,50 € |
| M5 | 5,00 € se odbija | nijedan test nema tačno 5,00 € | 5 km, 5,00 € → 3,00 € |
| M6 | 30,00 € plaća dostavu (D-01) | nijedan test nema tačno 30,00 € | 5 km, 30,00 € → 0,00 € |
| M8 | ispod 3 km dostava je jeftinija | jedini test ispod 3 km ima besplatnu dostavu | 1 km, 10,00 € → 2,00 € |
Primijetite nešto važno: četiri od pet preživjelih su granice (0 km, 10 km, 5,00 €, 30,00 €), a peti je oblast „ispod 3 km” koja nije bila testirana s plaćenom dostavom. Testovi iz poglavlja 6 bili su napravljeni da obiđu puteve, a ne da stoje na granicama. Upravo zato je sljedeće poglavlje, tehnike crne kutije, prvo posvećeno granicama.
Dodamo li test_granica.py iz poglavlja 6 (tačno 30,00 € i 29,99 €), M6 je ubijen, a rezultat raste na 67 % (8 od 12). Dodamo li test_jaki.py s pet testova iz posljednje kolone tabele:
cijena_dostave@pytest.mark.parametrize("km, iznos, ocekivano", [
(0, 1000, 200), # PP-3: 0 km je u zoni (ubija M1)
(10, 1000, 550), # PP-3: 10 km je u zoni (ubija M2)
(5, 500, 300), # PP-1: tačno 5,00 € se prihvata (ubija M5)
(5, 3000, 0), # PP-4: tačno 30,00 € je besplatno (ubija M6)
(1, 1000, 200), # PP-2: do 3 km je 2,00 € (ubija M8)
])
def test_granice(km, iznos, ocekivano):
assert cijena_dostave(km, iznos) == ocekivanopython mutanti.py ../pokrivenost/test_svi.py test_jaki.py
...
Ubijeno 12 od 12: rezultat mutacija 100 %
cijena_dostaveSlika 7.4 sažima pouku poglavlja 6 i 7: pokrivenost kaže da je kod izvršen, mutacije kažu da je provjeren.
Smislite trinaesti mutant za cijena_dostave koji bi preživio sve testove iz test_svi.py i test_jaki.py. – Jedan primjer: if iznos >= 3000: → if iznos >= 3000 and udaljenost_km < 9: (besplatna dostava samo do 9 km). Svi testovi s 30,00 € ili više su na 2,5 km ili 5 km, pa mutant preživljava. Test koji ga ubija: 9,5 km i 32,00 € → 0,00 € (PP-4 ne spominje udaljenost).
cijena_dostavecijena_dostave (58 %).Ponekad mutant ne može biti ubijen, jer izmjena ne mijenja ponašanje. Na primjer:
dodatni_km = math.ceil(max(0, udaljenost_km - 3)) # original
dodatni_km = max(0, math.ceil(udaljenost_km - 3)) # mutantZa svaku udaljenost oba reda daju isti broj (zaokruživanje naviše i odsijecanje negativnih brojeva mogu zamijeniti mjesta). To je ekvivalentan mutant: nijedan test ga ne može ubiti, i ne treba ga ni pokušavati. Ekvivalentni mutanti se izuzimaju iz računa, ali ih je teško automatski prepoznati, pa čovjek mora pogledati svaki preživjeli mutant i odlučiti: „rupa u testovima” ili „ekvivalentan”.
Ručno pravljenje mutanata dobro je za učenje. Za pravi projekat postoje alati koji sami prave stotine mutanata:
| Jezik | Alat |
|---|---|
| Python | mutmut, cosmic-ray |
| Java | PIT (pitest) |
| JavaScript, TypeScript, C# | Stryker |
Alati imaju i cijenu: ako testovi traju 10 sekundi, a mutanata je 500, cijeli krug traje skoro 1,5 sat. Zato se mutacije obično ne puštaju pri svakom push, nego jednom dnevno ili sedmično, ili samo nad izmijenjenim dijelom koda. (mutmut na Windowsu radi kroz WSL, Linux unutar Windowsa; skripta mutanti.py radi svuda.)
„Treba težiti 100 % rezultatu mutacija u cijelom projektu.” Kao i kod pokrivenosti, slijepa trka za brojem je skupa. Mutacije su najkorisnije za najvažnije funkcije: novac (cijena, popust, napojnica), sigurnost i prava pristupa.
cijena_dostave na granicama? Testovi su pravljeni da obiđu puteve, a ne da stoje tačno na granicama pravila.Odgovori: tipka R ili klik na zamućeno polje.
Cilj je pronaći i popraviti anomalije toka podataka, pokrenuti ruff i mypy, i podići rezultat mutacija funkcije cijena_dostave na 100 %. Radi se u parovima.
primjeri/tok_podataka/racun.py. Na papiru napravite tabelu: red, definicije, upotrebe (kao slika 7.2).pytest -q test_racun.py. Uporedite poruke s vašim anomalijama: koja poruka odgovara kojoj anomaliji?pip install ruff mypy.ruff check racun.py --output-format concise i mypy --enable-error-code possibly-undefined racun.py. Popunite tabelu iz 7.6 svojim rezultatima.--enable-error-code possibly-undefined. Koja poruka nestaje?ruff check --fix --unsafe-fixes racun.py na kopiji fajla. Šta je ruff uradio s napojnicom? Da li je greška ispravljena ili samo sakrivena?racun.py (u svom repozitoriju, ne u primjeri/) tako da ruff ne javlja ništa, mypy ne javlja ništa i sva četiri testa prolaze. Ne gledajte racun_ispravljen.py dok ne završite.racun_ispravljen.py. Pokrenite testove nad tom verzijom: $env:RACUN="ispravljen" (PowerShell) ili RACUN=ispravljen pytest (Linux, macOS).primjeri/deliveryapp pokrenite ruff check --isolated . --output-format concise. Zapišite nalaz i pretpostavku o tome šta on govori o prvoj verziji.ruff check --isolated --select C901 --config "lint.mccabe.max-complexity=3" . Koje funkcije imaju V(G) veće od 3? Uporedite s poglavljem 6.primjeri/mutacije pokrenite python mutanti.py ../pokrivenost/test_svi.py. Zapišite rezultat.test_jaki.py, napišite u svom fajlu test_moji_mutanti.py test koji ga ubija. Očekivani rezultat računajte iz pravila PP-1 … PP-4.python mutanti.py ../pokrivenost/test_svi.py test_moji_mutanti.py dok rezultat ne bude 100 %.MUTANTI u svojoj kopiji skripte dva nova mutanta (npr. udaljenost_km - 3 u udaljenost_km - 2, ili 500 u 499). Da li vaši testovi ubijaju i njih? Ako ne, dopišite test.Dajte AI asistentu funkciju napojnica iz primjeri/deliveryapp/napojnica.py i zatražite listu od 10 mutanata. Za svaki odlučite: da li ga 11 testova iz poglavlja 5 ubija? Provjerite tvrdnju tako što ćete mutant ručno unijeti u kopiju fajla i pokrenuti testove. Zapišite da li je AI predložio i neki ekvivalentan mutant, i da li je to sam primijetio.
Rezultat koji se predaje: fajl rezultati-7.md s tabelom definicija i upotreba funkcije racun i spiskom anomalija; tabela nalaza ruff, mypy i testova; ispravljen racun.py s tabelom parova definicija–upotreba; nalaz ruff nad DeliveryApp; test_moji_mutanti.py i ispis skripte sa 100 %; dva nova mutanta i jedan ekvivalentan s objašnjenjem.
Rješenja su sakrivena – tipka R ili klik na okvir.
Označite definicije i upotrebe i nađite anomalije:
def vrijeme(restoran, km):
if restoran == "R-1":
priprema = 20
if restoran == "R-2":
priprema = 15
voznja = 4 * math.ceil(km)
voznja = 5
return priprema + voznjapriprema: dvije definicije (u dva if), upotreba u return. Za restoran R-3 (ili nepoznat) nijedna definicija se ne izvrši, pa je to u bez d (UnboundLocalError). voznja: definisana računom, pa odmah prepisana brojem 5 bez čitanja: dd; vrijeme vožnje je uvijek 5 minuta, bez obzira na udaljenost. Po PP-9, R-1 na 4,2 km treba dati 20 + 4 × 5 = 40 minuta; ova funkcija daje 25.
Za ispravljenu funkciju racun iz 7.3 napravite najmanji skup testova koji pokriva kriterij sve definicije (svaka definicija stiže do bar jedne upotrebe). Koliko testova treba?
Treba da se izvrše d1(p), d2(p), d1(dostava), d2(dostava) i d(napojnica), svaka do return. d1(p) i d1(dostava) se izvršavaju uvijek, ali „stižu” do return samo ako ih ne prepiše druga definicija. Dva testa su dovoljna: N-1001 bez koda (d1(p), d1(dostava) i napojnica stižu do return) i 32,00 € na 2,5 km sa STUDENT10 (d2(p) i d2(dostava)). Očekivano: 24,50 € i 32,00 € − 3,20 € + 0,00 € = 28,80 €.
Šta znače poruke i šta treba uraditi?
deliveryapp.py:31:5: F841 Local variable `procenat` is assigned to but never used
narudzba.py:12: error: Argument 1 to "eur" has incompatible type "str"; expected "int" [arg-type]Prva: u redu 31 promjenljiva procenat je izračunata, a nikad korištena (d bez u); treba provjeriti da li je zaboravljena u računu popusta, a ne samo obrisati red. Druga: u redu 12 funkciji eur je poslan tekst (npr. „2450” iz forme), a očekuje cijeli broj centi; treba pretvoriti ulaz u broj (int(...)) i provjeriti šta se desi s pogrešnim unosom.
Alat je napravio 40 mutanata. Testovi su ubili 30, preživjelo je 10, a pregledom je utvrđeno da su 4 od preživjelih ekvivalentni. Koliki je rezultat mutacija? Šta treba uraditi s preostalih 6?
30 / (40 − 4) = 30 / 36 ≈ 83 %. Preostalih 6 su rupe u testovima: za svakog napisati test koji ga ubija, s očekivanim rezultatom iz pravila.
Za funkciju popust iz deliveryapp.py (return min(round(iznos * procenat / 100), MAX_POPUST)) napravite mutant koji glumi grešku D-05 i test koji ga ubija.
Mutant: return round(iznos * procenat / 100) (obrisan min, nema gornje granice). Test: assert popust(8000, "STUDENT10") == 500. Na 80,00 € mutant daje 800 centi (8,00 €), a PP-5 kaže najviše 5,00 €. Test s 21,50 € ga ne bi ubio, jer je tamo popust 2,15 € i bez granice.
racun značile pad bez koda, uvijek besplatnu dostavu i izgubljenu napojnicu.dostava = 0.cijena_dostave sa 100 % pokrivenosti ubili su samo 58 % mutanata, jer su preživjeli mutanti bili na granicama pravila, a pet graničnih testova podiglo je rezultat na 100 %.racun i koju poslovnu grešku izaziva svaka?racun.py?test_svi.py i zašto je preživjelo pet mutanata?