Poglavlje 12: Testiranje paralelnih aplikacija
Priča za početak. Četvrtog jula 1997. NASA-in Mars Pathfinder spustio se na Mars, odskakujući po površini u velikim vazdušnim jastucima, i počeo slati prve slike i podatke. Nekoliko dana kasnije desilo se nešto čudno: računar letjelice se sam od sebe ponovo pokretao, i pri tome gubio dio prikupljenih podataka. Na Zemlji, u laboratoriji JPL, inženjeri su imali kopiju letjelice i pokretali isti softver satima, dok nisu uhvatili trenutak u kojem se greška javlja. Uzrok je bio problem poznat iz udžbenika, ali rijedak u praksi: inverzija prioriteta. Zadatak niskog prioriteta (prikupljanje meteoroloških podataka) zaključao je zajednički resurs. Zadatak visokog prioriteta (upravljanje unutrašnjom sabirnicom podataka) čekao je da se resurs oslobodi. A između njih se uglavio zadatak srednjeg prioriteta (komunikacija), koji je dugo radio i nije puštao niski zadatak da završi i oslobodi bravu. Visoki zadatak je čekao predugo, nadzorni mehanizam je zaključio da nešto ne valja i ponovo pokrenuo računar. Rješenje je bilo već ugrađeno u operativni sistem, samo nije bilo uključeno: nasljeđivanje prioriteta. Inženjeri su promjenu poslali sa Zemlje, na Mars, i letjelica je radila dalje. Šest godina kasnije, 14. avgusta 2003., sličan, ali skuplji problem: u kontrolnom centru jedne elektroprivrede u Ohaju trka podataka u alarmnom softveru tiho je zaustavila alarme. Operateri više od sat vremena nisu vidjeli upozorenja da dalekovodi ispadaju jedan za drugim, a kvarovi su se proširili na veliki dio sjeveroistoka SAD-a i Ontarija. Bez struje je ostalo oko 50 miliona ljudi. Obje priče imaju isti zaključak: greške u programima koji rade više stvari istovremeno javljaju se rijetko, nepredvidivo i baš onda kad je najgore. Ovo poglavlje uči kako ih testirati.
U poglavlju 11 vidjeli smo da se stanje objekta mora slagati sa stvarnošću i da je Therac-25 pao pri brzom unosu: dva događaja koja su se desila „u isto vrijeme”. Sada se time bavimo direktno. Prvo učimo šta su niti i kako nastaje trka podataka, na brojaču narudžbi DeliveryApp. Zatim zastoj i inverziju prioriteta. Na kraju nestabilne testove: testove koji ponekad prolaze, a ponekad padaju, i tehnike kojima se paralelne greške ipak mogu uhvatiti.
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: G. Reeves: „What really happened on Mars?”, JPL, 1997 (pismo inženjera Pathfindera); U.S.-Canada Power System Outage Task Force: Final Report on the August 14, 2003 Blackout, 2004; dokumentacija modula
threading(docs.python.org).
Riječi ovog poglavlja.
- Paralelno izvršavanje – više dijelova programa radi u isto vrijeme (ili tako izgleda, jer se brzo smjenjuju).
- Nit (thread) – jedan tok izvršavanja unutar programa; program može imati više niti koje dijele istu memoriju.
- Dijeljeni resurs – podatak ili uređaj koji koristi više niti, npr. brojač narudžbi.
- Trka podataka (race condition) – rezultat zavisi od slučajnog redoslijeda u kojem niti pristupaju dijeljenom resursu.
- Kritična sekcija – dio koda koji smije izvršavati samo jedna nit u isto vrijeme.
- Brava (lock, mutex) – mehanizam koji pušta samo jednu nit u kritičnu sekciju.
- Atomska operacija – operacija koja se izvrši u jednom nedjeljivom koraku; druga nit ne može „upasti” u sredinu.
- Zastoj (deadlock) – dvije ili više niti čekaju jedna drugu zauvijek.
- Inverzija prioriteta – zadatak visokog prioriteta čeka zadatak niskog prioriteta, kojeg prekida zadatak srednjeg prioriteta.
- Nasljeđivanje prioriteta – nit koja drži bravu privremeno dobija prioritet najvažnije niti koja na tu bravu čeka.
- Nestabilan test (flaky test) – test koji nad istim kodom nekad prolazi, a nekad pada.
- Heisenbug – greška koja nestaje ili se mijenja kad je pokušate posmatrati (npr. dodavanjem ispisa).
Ciljevi poglavlja. Nakon poglavlja možete: objasniti šta su niti i zašto dijeljeni podaci prave probleme; pokazati na primjeru kako nastaje trka podataka; spriječiti trku bravom i provjeriti ispravku testom; objasniti zašto običan test trku obično ne vidi; napisati test koji pokreće više niti i provjerava rezultat; prepoznati zastoj i napisati test koji ga otkriva bez da visi; objasniti inverziju prioriteta i nasljeđivanje prioriteta; nabrojati uzroke nestabilnih testova i načine kako se s njima postupa.
Plan poglavlja – Sedmica 12: tri časa teorije i tri časa praktične nastave
| Čas | Sadržaj | Na času |
|---|---|---|
| Teorija 1 | 12.1–12.3 Niti i dijeljeni podaci; trka podataka; brava | Dva kupca, isti broj narudžbe |
| Teorija 2 | 12.4–12.6 Zastoj; inverzija prioriteta; kako se testira paralelni kod | Kuhinja i dostava čekaju jedna drugu |
| Teorija 3 | 12.7–12.9 Nestabilni testovi; uzroci; šta raditi s njima | Test koji pada svaki deseti put |
| Praksa 1–3 | Brojač s nitima; ponavljanje testa; lov na nestabilne testove | primjeri/paralelno/ |
Dio 1 – Niti i trka podataka
12.1 Više stvari u isto vrijeme
Petak je, 19 sati. Stotine kupaca istovremeno naručuju na DeliveryApp. Server ne može čekati da prvi kupac završi pa tek onda primiti drugog: on istovremeno obrađuje mnogo zahtjeva, svaki u svojoj niti. Slika iz života: kuhinja restorana. Jedan kuhar je jedna nit. Više kuhara radi istovremeno, brže, ali dijele isti frižider, istu tablu s narudžbama i isti šporet. Dok kuhari ne diraju iste stvari, sve je u redu. Problem nastaje kad dvojica u istom trenutku posegnu za posljednjim jajetom.
Za testera, paralelni programi imaju tri neugodne osobine:
- Redoslijed nije zadan. Operativni sistem sam odlučuje koja nit radi kad, i to se mijenja od pokretanja do pokretanja.
- Greške su rijetke. Pogrešan redoslijed se može desiti jednom u hiljadu ili milion pokretanja.
- Posmatranje mijenja ponašanje. Dodate li
printda vidite šta se dešava, promijeni se brzina i greška nestane (zato se zovu „Heisenbug”, po fizičaru Heisenbergu i njegovom principu neodređenosti).
Ukratko.
- Server obrađuje mnogo zahtjeva istovremeno, svaki u svojoj niti, a niti dijele istu memoriju.
- Problem nastaje kad više niti istovremeno koristi isti podatak.
- Redoslijed niti nije zadan i mijenja se od pokretanja do pokretanja.
- Paralelne greške su rijetke i često nestaju kad se pokušaju posmatrati.
12.2 Trka podataka: isti broj za dvije narudžbe
Svaka narudžba dobija broj: N-1001, N-1002, … Brojač u primjeri/paralelno/brojac.py:
class Brojac:
"""Brojač BEZ brave: čitanje i upis nisu jedan nedjeljiv korak."""
def __init__(self, pocetak=1000):
self.zadnji = pocetak
def sljedeci(self):
procitano = self.zadnji # 1. pročitaj
time.sleep(0) # (ovdje sistem može prebaciti na drugu nit)
self.zadnji = procitano + 1 # 2. upiši
return f"N-{self.zadnji}"Metoda ima dva koraka: pročitaj zadnji broj, pa upiši sljedeći. U jednoj niti to je savršeno ispravno. Ali pogledajte šta se može desiti s dvije niti (slika 12.1):
- nit A pročita 1000;
- sistem prebaci na nit B, koja takođe pročita 1000;
- nit A upiše 1001 i vrati N-1001;
- nit B upiše 1001 i vrati N-1001.

Dva kupca dobiju isti broj narudžbe. Restoran pripremi jednu, dostavljač odnese pogrešnu, a jedna uplata nema svoju narudžbu. To je trka podataka: rezultat zavisi od toga koja nit „pobijedi” u trci.
Red time.sleep(0) ne čeka ništa, ali kaže sistemu „sad možeš prebaciti na drugu nit”. Ubacili smo ga da trka postane česta, da je vidimo na času. Funkcija podijeli_brojeve pokrene 8 niti i svaka traži 200 brojeva, ukupno 1600. Pet pokretanja:
dobijeno brojeva: 1600 1600 1600 1600 1600
različitih: 385 323 395 364 344
Od 1600 narudžbi samo oko 350 ima jedinstven broj.
A bez reda time.sleep(0), s običnim self.zadnji += 1? Izmjerili smo 200 pokretanja: nijedan dupli broj. Znači li to da je kod ispravan? Ne. Operacija += nad atributom nije atomska: Python je izvršava u nekoliko koraka, i sistem može prebaciti nit između njih. U našem mjerenju se to nije desilo, ali na drugom računaru, drugoj verziji Pythona, pod većim opterećenjem, ili jednom u milion narudžbi, desiće se. Ovo je najvažnija pouka poglavlja: paralelna greška koju test ne vidi i dalje postoji.
Na času. Pogledajte
primjeri/deliveryapp/api.py, funkcijunova_narudzba:BROJAC[0] += 1, pabroj = f"N-{BROJAC[0]}". FastAPI obične (def) funkcije izvršava u više niti istovremeno. Ima li ovdje trke? – Ima: dvije niti mogu povećati brojač, pa obje pročitati istu, veću vrijednost, i dobiti isti broj. API iz poglavlja 9 ima istu skrivenu grešku kao naš brojač bez brave.
Ukratko.
- Trka podataka nastaje kad više niti čita i mijenja isti podatak, pa rezultat zavisi od redoslijeda.
- Brojač koji odvojeno čita i upisuje može dati isti broj dvjema narudžbama.
- Uz
time.sleep(0)trka je česta: od 1600 brojeva samo oko 350 je jedinstveno.- Bez te linije 200 pokretanja nije pokazalo grešku, ali greška i dalje postoji.
12.3 Brava i test s mnogo niti
Rješenje je da „pročitaj i upiši” postane kritična sekcija: dio koda u kojem smije biti samo jedna nit. Za to služi brava (threading.Lock). Nit koja uđe u blok with self.brava: zaključa bravu; druga nit koja pokuša ući čeka dok prva ne izađe.
class BrojacSaBravom(Brojac):
"""Brojač S bravom: samo jedna nit u isto vrijeme radi „pročitaj i upiši”."""
def __init__(self, pocetak=1000):
super().__init__(pocetak)
self.brava = threading.Lock()
def sljedeci(self):
with self.brava:
return super().sljedeci()Slika iz života: jedan ključ od toaleta na benzinskoj pumpi. Ko ima ključ, unutra je; ostali čekaju, ma koliko se žurili.
Kako to testirati? Običan test s jednom niti je beskoristan: i brojač bez brave prolazi.
def test_jedna_nit_bez_brave_radi():
"""U jednoj niti i brojač bez brave je ispravan: zato običan test grešku ne vidi."""
b = Brojac()
assert [b.sljedeci() for _ in range(3)] == ["N-1001", "N-1002", "N-1003"]Test paralelnog koda mora napraviti paralelnost: pokrenuti mnogo niti, pustiti ih da se „sudaraju”, i provjeriti svojstvo koje mora važiti bez obzira na redoslijed. Za brojač: svi brojevi su različiti, ima ih tačno 1600, a najveći je N-2600.
@pytest.mark.parametrize("ponavljanje", range(20))
def test_sa_bravom_svi_brojevi_razliciti(ponavljanje):
brojevi = podijeli_brojeve(BrojacSaBravom(), niti=8, po_niti=200)
assert len(brojevi) == 1600
assert len(set(brojevi)) == 1600 # nijedan broj nije dat dvaput
assert max(brojevi, key=lambda b: int(b[2:])) == "N-2600"Tri tehnike u jednom testu:
- mnogo niti i mnogo operacija (8 × 200), da se poveća šansa sudara;
- provjera svojstva, a ne tačnog redoslijeda (ne znamo koja nit dobija koji broj, ali znamo da su svi različiti);
- ponavljanje (
range(20)), jer jedno prolaženje paralelnog testa ne dokazuje mnogo.
Za brojač bez brave postoji isti test, označen sa xfail („očekujemo da padne”), da u izvještaju ostane vidljivo da je ta verzija pogrešna:
pytest -q -rx test_brojac.py
XFAIL test_brojac.py::test_bez_brave_svi_brojevi_razliciti - trka podataka: brojač bez brave daje iste brojeve
22 passed, 1 xfailed in 0.79s
Česta zabluda. „Python ima GIL (globalnu bravu interpretera), pa trke u Pythonu nema.” GIL samo osigurava da jedna nit u jednom trenutku izvršava Python instrukciju. Operacija kao
self.zadnji += 1ima više instrukcija, i sistem može prebaciti nit između njih. Trka u Pythonu postoji, samo je rjeđa, a to je, za testera, još gore.
Ukratko.
- Brava pušta samo jednu nit u kritičnu sekciju i tako sprečava trku.
- Test s jednom niti ne vidi trku: i brojač bez brave ga prolazi.
- Test paralelnog koda pokreće mnogo niti, provjerava svojstvo koje ne zavisi od redoslijeda i ponavlja se.
- Oznaka
xfailčuva u izvještaju test koji namjerno pokazuje poznatu grešku.
Provjeri se.
- Šta je trka podataka? – Greška u kojoj rezultat zavisi od slučajnog redoslijeda kojim niti pristupaju istom podatku.
- Zašto običan test brojača ne otkriva trku? – Jer radi u jednoj niti, a trka nastaje tek kad više niti istovremeno čita i upisuje.
- Koje svojstvo provjerava test brojača s nitima? – Da su svi dobijeni brojevi različiti i da ih ima tačno 1600.
Dio 2 – Zastoj i inverzija prioriteta
12.4 Zastoj
Brava rješava trku, ali uvodi novu opasnost. Zamislite dvije brave, kuhinja i dostava. Nit A (restoran) zaključa kuhinju, pa traži dostavu. Nit B (dostavljač) zaključa dostavu, pa traži kuhinju. Svaka drži ono što onoj drugoj treba, i obje čekaju zauvijek. To je zastoj (deadlock). Slika iz života: dva auta na uskom mostu, jedan sa svake strane; nijedan ne želi da se vrati.
Za zastoj moraju važiti četiri uslova zajedno (tzv. Coffmanovi uslovi): resurs može držati samo jedna nit; nit drži jedan resurs dok čeka drugi; resurs se ne može silom oduzeti; i niti čekaju u krug. Najčešći lijek je ukloniti krug: sve niti zaključavaju brave uvijek istim redom (npr. uvijek prvo kuhinja, pa dostava).
Kako testirati zastoj, a da test ne visi zauvijek? Svako čekanje u testu mora imati vremensko ograničenje. Test iz test_brojac.py namjerno pravi zastoj i provjerava da ga je otkrio:
def test_zastoj_otkriven_vremenskim_ogranicenjem():
kuhinja, dostava = threading.Lock(), threading.Lock()
obje_drze_prvu, obje_pokusale = threading.Barrier(2), threading.Barrier(2)
uspjeh = []
def nit(prva, druga):
with prva:
obje_drze_prvu.wait() # obje niti sada drže po jednu bravu
dobila = druga.acquire(timeout=0.5) # čeka drugu bravu najviše pola sekunde
uspjeh.append(dobila)
obje_pokusale.wait() # prvu bravu pušta tek kad su obje pokušale
if dobila:
druga.release()
a = threading.Thread(target=nit, args=(kuhinja, dostava))
b = threading.Thread(target=nit, args=(dostava, kuhinja))
a.start(); b.start(); a.join(); b.join()
assert uspjeh == [False, False] # nijedna nit nije dobila drugu bravu: zastojDva detalja su važna. Barijera (threading.Barrier(2)) tjera obje niti da sačekaju jedna drugu na istom mjestu, pa se zastoj dešava svaki put, a ne slučajno: test je ponovljiv. A acquire(timeout=0.5) umjesto običnog acquire() znači da test u najgorem slučaju traje pola sekunde, a ne beskonačno.
U stvarnim projektima isto važi za cijelu svitu: alat kao pytest-timeout prekida svaki test koji traje predugo i prijavi ga kao pad, umjesto da CI čeka satima.
Ukratko.
- Zastoj nastaje kad niti čekaju jedna drugu u krug, npr. dvije brave zaključane obrnutim redom.
- Najčešći lijek je da sve niti zaključavaju brave istim redom.
- Svako čekanje u testu mora imati vremensko ograničenje, da test ne visi zauvijek.
- Barijera čini paralelni scenarij ponovljivim, jer tjera niti da se sretnu na istom mjestu.
12.5 Inverzija prioriteta
Na Marsu se desila treća vrsta problema. U sistemima stvarnog vremena (letjelice, automobili, medicinski uređaji) zadaci imaju prioritete: važniji zadatak prekida manje važan. To je dobro, ali u kombinaciji s bravom stvara zamku (slika 12.2):
- zadatak niskog prioriteta (meteo podaci) zaključa bravu nad zajedničkom sabirnicom;
- zadatak visokog prioriteta (upravljanje sabirnicom) treba istu bravu, pa čeka;
- zadatak srednjeg prioriteta (komunikacija), kojem brava ne treba, prekine niski zadatak i radi dugo;
- niski zadatak ne može završiti i pustiti bravu, pa visoki čeka srednji, iako je važniji od njega;
- nadzorni brojač (watchdog) primijeti da visoki zadatak predugo nije završio i resetuje računar.

Rješenje je nasljeđivanje prioriteta: dok niski zadatak drži bravu na koju čeka visoki, on privremeno dobija visoki prioritet. Srednji ga više ne može prekinuti, niski brzo završi, pusti bravu, i visoki nastavlja. Operativni sistem Pathfindera to je znao raditi, ali opcija nije bila uključena za tu bravu. Inženjeri su je uključili porukom poslanom na Mars.
Zašto testovi na Zemlji nisu našli grešku prije lansiranja? Prema kasnijem pismu vođe softverskog tima, ponovna pokretanja su se tokom testiranja javila, ali rijetko; nisu se mogla ponoviti ni objasniti, a prednost je dobio posao koji je tada izgledao važniji, softver za slijetanje. Na Marsu, s više podataka i drugačijim rasporedom poslova, javljala su se češće. Pouka za testera: rijetka, neobjašnjena greška u paralelnom sistemu nije šum, nego trag.
Iz stvarnog svijeta. Nestanak struje 2003. ima sličnu pouku. Istražna komisija utvrdila je da je trka podataka u alarmnom softveru zaustavila obradu alarma, a sistem to nije javio: operateri su gledali ekran koji je izgledao normalno. Najopasnija paralelna greška nije ona koja ruši program, nego ona koja ga tiho zaustavi.
Ukratko.
- Inverzija prioriteta nastaje kad visoki zadatak čeka bravu koju drži niski, a niskog prekida srednji.
- Na Pathfinderu je zbog toga nadzorni brojač stalno resetovao računar.
- Nasljeđivanje prioriteta privremeno daje niskom zadatku visoki prioritet dok drži bravu.
- Rijetka, neobjašnjena greška u paralelnom sistemu je trag, a ne šum.
12.6 Kako se testira paralelni kod
Paralelne greške se ne mogu „isključiti” testom kao što se može provjeriti cijena N-1001. Ali šansa da ih nađemo može se jako povećati. Tehnike, od najjednostavnije:
| Tehnika | Kako | U DeliveryApp |
|---|---|---|
| stres s nitima | mnogo niti, mnogo operacija istovremeno | 8 niti × 200 brojeva |
| ponavljanje | isti test stotine ili hiljade puta | range(20); dodatak pytest-repeat (--count=1000) |
| provjera svojstva | provjerava se ono što mora važiti uvijek, ne tačan redoslijed | svi brojevi različiti |
| ubrzavanje prebacivanja | ubaciti tačke u kojima sistem prebacuje niti | time.sleep(0) između čitanja i upisa |
| barijere | niti se sinhronizuju na tačno određenom mjestu, pa je scenarij ponovljiv | test zastoja |
| vremenska ograničenja | svako čekanje ima rok | acquire(timeout=0.5), pytest-timeout |
| statička analiza i recenzija | tražiti dijeljene podatke bez brave čitanjem koda | BROJAC[0] += 1 u api.py |
| test opterećenja | stotine pravih korisnika istovremeno | Locust iz poglavlja 10, s provjerom jedinstvenih brojeva |
Najvažnija rečenica: jedan prolaz paralelnog testa ne dokazuje skoro ništa, a jedan pad dokazuje da greška postoji. Zato se paralelni testovi ponavljaju, a svaki njihov pad se istražuje, nikad ne ignoriše.
Postoji i dizajnerski savjet koji štedi najviše posla: što manje dijeljenih podataka. Brojač narudžbi u pravom sistemu obično se prepušta bazi podataka (npr. automatski rastući ključ ili sekvenca), koja je napravljena da radi s mnogo istovremenih korisnika i testirana decenijama. Najbolja trka je ona koju ne moramo testirati jer nema dijeljenog podatka.
Ukratko.
- Paralelne greške se ne mogu isključiti testom, ali se šansa da se nađu može jako povećati.
- Tehnike su stres s nitima, ponavljanje, provjera svojstva, ubrzavanje prebacivanja, barijere i vremenska ograničenja.
- Jedan prolaz paralelnog testa ne dokazuje mnogo, a jedan pad dokazuje da greška postoji.
- Najbolje je imati što manje dijeljenih podataka, npr. brojeve narudžbi prepustiti bazi.
Provjeri se.
- Koja četiri uslova moraju važiti za zastoj? – Isključiv pristup resursu, držanje jednog resursa dok se čeka drugi, nemogućnost oduzimanja resursa i čekanje u krug.
- Kako je riješen problem Pathfindera? – Uključenjem nasljeđivanja prioriteta za bravu, porukom poslanom sa Zemlje.
- Zašto svako čekanje u testu mora imati vremensko ograničenje? – Da test pri zastoju ne visi zauvijek, nego se završi i prijavi pad.
Dio 3 – Nestabilni testovi
12.7 Test koji ponekad pada
Nestabilan test (flaky test) nad istim kodom nekad prolazi, a nekad pada. Paralelne greške su jedan uzrok, ali ne jedini. Nestabilni testovi su veliki problem u industriji: veliki timovi javno izvještavaju da značajan dio padova u njihovom CI-ju nisu prave greške, nego nestabilni testovi. Kad tim navikne da „crveno ponekad znači ništa”, prestane vjerovati testovima, i prava greška prođe, kao poruka „MALFUNCTION 54” kod Therac-25 koju su operateri navikli preskakati.
Najčešći uzroci, s primjerima iz DeliveryApp:
| Uzrok | Primjer | Lijek |
|---|---|---|
| paralelnost | brojač bez brave | brava; test s nitima koji se ponavlja |
| vrijeme | test radnog vremena prolazi samo prije 23 h | patch trenutnog vremena (poglavlje 11) |
| redoslijed testova | test očekuje broj N-1001, ali neki raniji test je već napravio narudžbu | svaki test sam pravi svoje podatke; fixture |
| dijeljeno stanje | globalni rječnik NARUDZBE u api.py puni se iz testa u test |
izolacija: novo stanje za svaki test |
| čekanje „na oko” | time.sleep(1) pa provjera; na sporom računaru 1 s nije dovoljno |
čekati uslov, ne vrijeme (poglavlje 13) |
| slučajnost | test koristi random bez sjemena |
fiksno sjeme (poglavlje 9) |
| spoljni servisi i mreža | test zove pravi servis mapa koji ponekad kasni | dvojnik u jediničnim testovima; ponovni pokušaj u sistemskim |
| decimalni brojevi i zaokruživanje | poređenje 0.1 + 0.2 == 0.3 |
centi (poglavlje 5) |
Primjer nestabilnog testa zbog redoslijeda:
def test_prva_narudzba_ima_broj_1001():
r = klijent.post("/v1/narudzbe", json=N1001)
assert r.json()["broj"] == "N-1001" # tačno samo ako je ovo prva narudžba u cijeloj sesijiPokrenut sam, prolazi. Pokrenut s ostalim testovima API-ja, pada, jer je BROJAC već povećan. Ispravno: provjeriti svojstvo (broj počinje sa „N-” i različit je od prethodnog), ili da svaki test dobija novu, praznu aplikaciju.
Ukratko.
- Nestabilan test nad istim kodom nekad prolazi, a nekad pada.
- Uzroci su paralelnost, vrijeme, redoslijed testova, dijeljeno stanje, čekanje na oko, slučajnost i mreža.
- Nestabilni testovi uče tim da ignoriše crveno, pa prava greška prođe.
- Test koji zavisi od drugih testova ili od trenutnog vremena treba prepraviti tako da sam pravi svoje uslove.
12.8 Kako naći nestabilan test
Nestabilan test se teško hvata, jer kad ga pogledate, prolazi. Alati pomažu:
- ponavljanje:
pytest --count=200 tests/test_api.py(dodatak pytest-repeat) pokrene svaki test 200 puta; test koji padne makar jednom je nestabilan; - nasumičan redoslijed: dodatak pytest-randomly, čim je instaliran, svaki put izmiješa redoslijed testova i ispiše sjeme (
-p randomly:SJEMEga ponavlja,-p no:randomlyisključuje); test koji pada samo u nekom redoslijedu zavisi od drugog testa; - paralelno izvršavanje testova: dodatak pytest-xdist (
pytest -n 4) pokreće testove u četiri procesa istovremeno; otkriva testove koji dijele fajlove, portove ili bazu; - istorija u CI-ju: većina CI sistema pamti rezultat svakog testa; test koji je u posljednjih 100 pokretanja pao 3 puta, bez promjene koda, je nestabilan.
Kad se nestabilan test nađe, ponovite ga lokalno, mnogo puta, s istim uslovima (isto sjeme, isti redoslijed), dok ne nađete uzrok. Zapišite pad kao defekt, s tačnim uslovima, kao u poglavlju 3.
Ukratko.
- Ponavljanje testa stotinama puta otkriva da li je nestabilan.
- Nasumičan redoslijed otkriva testove koji zavise jedan od drugog.
- Paralelno izvršavanje testova otkriva testove koji dijele fajlove, portove ili bazu.
- Istorija rezultata u CI-ju pokazuje testove koji padaju bez promjene koda.
12.9 Šta raditi s nestabilnim testom
Tri pogrešna odgovora, koja se u praksi često čuju:
- „Pokreni ponovo, sad će proći.” Možda hoće, ali greška (u testu ili u kodu) ostaje. Ako je uzrok trka u kodu, upravo ste je sakrili.
- „Obriši test.” Gubite i ono što je test dobro provjeravao.
- „Dodaj
time.sleep(5).” Test postaje spor i i dalje nestabilan na sporijem računaru.
Dobar postupak:
- Istražiti: da li je greška u testu (loš test) ili u kodu (prava trka)? Paralelni pad je uvijek sumnjiv na pravu grešku dok se ne dokaže suprotno.
- Karantin: ako se ne može odmah popraviti, test se označi (npr.
@pytest.mark.skip(reason="nestabilan, defekt D-123")ili posebna oznaka) i zapiše kao defekt s vlasnikom i rokom. Tako ne blokira tim, a ne nestaje iz vida. - Popraviti uzrok: bravu u kodu, fixture umjesto dijeljenog stanja,
patchumjesto pravog vremena, čekanje uslova umjestosleep. - Dokazati popravku ponavljanjem: test koji je ranije padao jednom u 50 pokretanja mora proći, recimo, 1000 puta zaredom.
Česta zabluda. „Nestabilan test je problem testa, a ne aplikacije.” Ponekad jeste. Ali test koji ponekad pada zbog trke u kodu ispravno prijavljuje pravu grešku, koja će se u produkciji desiti mnogo češće, jer ima mnogo više korisnika. Brojač bez brave „ponekad” daje isti broj; u petak u 19 h to „ponekad” postaje „svakih par minuta”.
Ukratko.
- Ponovno pokretanje, brisanje i dodavanje pauze ne rješavaju nestabilan test.
- Nestabilan test se istražuje, po potrebi stavlja u karantin uz zapisan defekt, pa popravlja.
- Popravka se dokazuje ponavljanjem testa mnogo puta zaredom.
- Paralelni pad je sumnjiv na pravu grešku u kodu dok se ne dokaže suprotno.
Provjeri se.
- Navedite tri uzroka nestabilnih testova. – Npr. paralelnost, trenutno vrijeme, redoslijed testova (dijeljeno stanje), čekanje fiksnim
sleep, slučajnost bez sjemena, mreža.- Šta otkriva dodatak pytest-randomly? – Testove koji zavise od drugih testova, jer svaki put izmiješa njihov redoslijed.
- Šta znači staviti test u karantin? – Privremeno ga isključiti iz obaveznog prolaza, uz zapisan defekt s vlasnikom i rokom, dok se ne popravi.
Praktična nastava – Niti, zastoji i nestabilni testovi
Cilj je izazvati i spriječiti trku podataka, napraviti i otkriti zastoj, i pronaći nestabilne testove u DeliveryApp. Radi se u parovima. Prije početka: pip install pytest-repeat pytest-randomly pytest-timeout.
Korak 1 – Trka (45 minuta)
- U
primjeri/paralelnopokrenitepytest -v -rx test_brojac.py. Zapišite rezultate. - U Python konzoli pokrenite
podijeli_brojeve(Brojac())pet puta i zapišite broj jedinstvenih brojeva. Zatim pet puta saBrojacSaBravom(). - U svojoj kopiji
brojac.pyobrišite redtime.sleep(0). Pokrenite test bez brave 200 puta (pytest --count=200 -k bez_brave). Koliko puta je pao? Objasnite zašto to ne znači da je kod ispravan. - Pokušajte broj niti povećati na 32, a broj brojeva po niti na 2000 (bez
sleep). Da li se trka sada pojavljuje?
Korak 2 – Brojač u API-ju (30 minuta)
- Napišite test koji pokrene 8 niti, a svaka preko
TestClientnapravi 50 narudžbi (POST /v1/narudzbe). Provjerite da su svi brojevi različiti. Ponovite 20 puta. - Ispravite
api.pyu svojoj kopiji bravom oko povećanja i čitanja brojača. Ponovite test. - Zapišite nalaz kao izvještaj o defektu: naslov, koraci, očekivano, dobijeno, ozbiljnost.
Korak 3 – Zastoj (30 minuta)
- Pokrenite
test_zastoj_otkriven_vremenskim_ogranicenjem. Zatim u kopiji promijenite redoslijed brava u drugoj niti tako da obje zaključavaju kuhinja pa dostava. Šta se desi s testom? Ispravite tvrdnju u testu tako da provjerava da zastoja nema. - Napišite test u kojem nit čeka bravu bez vremenskog ograničenja i pokrenite ga sa
pytest --timeout=5. Šta ispisuje pytest-timeout?
Korak 4 – Lov na nestabilne testove (45 minuta)
- U
primjeri/deliveryapppokrenitepytest teststri puta (pytest-randomly svaki put miješa redoslijed i na vrhu ispiše sjeme), ipytest --count=20 tests/test_api.py. Da li neki test pada samo ponekad? - Dodajte namjerno nestabilan test
test_prva_narudzba_ima_broj_1001iz 12.7 u svoju kopijutest_api.py. Pokrenite ga sam, pa sa ostalim testovima, pa nekoliko puta s nasumičnim redoslijedom. Zapišite rezultate i prepravite test tako da provjerava svojstvo. - Napišite test radnog vremena bez
patch(koristi pravo trenutno vrijeme). U koje doba dana pada? Objasnite zašto je nestabilan i popravite ga.
Rezultat koji se predaje: fajl rezultati-12.md s mjerenjima brojača sa i bez brave, sa i bez time.sleep(0); test i izvještaj o defektu za brojač u API-ju; izmijenjen test zastoja; spisak nestabilnih testova koje ste našli, s uzrokom i popravkom.
Vježbe
Vježba za studenta 1 – trka.
Dvije niti istovremeno izvršavaju stanje_racuna = stanje_racuna - iznos za račun od 100 € (svaka plaća 30 €). Koja su sve moguća konačna stanja i koja je ispravna?
Rješenje. Ispravno je 40 € (100 − 30 − 30). Ako obje niti pročitaju 100 prije nego što ijedna upiše, obje upišu 70, pa je konačno stanje 70 €: 30 € je „nestalo” iz evidencije. Moguća stanja su 40 € i 70 €. Lijek: brava oko čitanja i upisa, ili atomska operacija u bazi.
Vježba za studenta 2 – svojstvo.
Funkcija istovremeno (8 niti) dodaje stavke u korpu. Koje svojstvo test treba provjeriti?
Rješenje. Da je broj stavki u korpi jednak zbiru svih dodatih, i da je ukupni iznos jednak zbiru cijena svih dodatih stavki (invarijanta korpe iz poglavlja 11), bez obzira na redoslijed. Ne provjerava se redoslijed stavki, jer on nije zadan.
Vježba za studenta 3 – zastoj.
Nit „restoran” zaključava bravu narudžbe pa bravu dostavljača; nit „dostavljač” zaključava bravu dostavljača pa bravu narudžbe. Kako spriječiti zastoj?
Rješenje. Dogovoriti isti redoslijed za sve niti, npr. uvijek prvo brava narudžbe, pa dostavljača. Tada nema čekanja u krug. Druga mogućnost je zaključavanje s vremenskim ograničenjem i ponovnim pokušajem, ali isti redoslijed je jednostavniji i sigurniji.
Vježba za studenta 4 – nestabilan test.
Test provjerava da se nakon klika na „Naruči” poruka „Narudžba primljena” pojavi poslije time.sleep(2). Na računaru programera uvijek prolazi, u CI-ju pada otprilike svaki deseti put. Uzrok i lijek?
Rješenje. Uzrok je čekanje „na oko”: na sporijem CI serveru odgovor ponekad stigne poslije 2 s. Lijek nije veći sleep, nego čekanje uslova: provjeravati svakih 0,1 s da li se poruka pojavila, do najviše npr. 10 s. Playwright to radi sam (poglavlje 13).
Vježba za studenta 5 – Pathfinder.
Zašto je nasljeđivanje prioriteta riješilo problem, a ne bi ga riješilo npr. povećanje prioriteta zadatka za meteo podatke na stalno visok?
Rješenje. Nasljeđivanje daje niskom zadatku visok prioritet samo dok drži bravu na koju čeka visoki zadatak; poslije se vraća na nizak i ne smeta drugim poslovima. Stalno visok prioritet za meteo zadatak bi ga stavio ispred komunikacije i drugih važnih poslova uvijek, pa bi se pomjerio problem: neki drugi važan zadatak bi kasnio. Nasljeđivanje rješava tačno uzrok (čekanje na bravu), a ne mijenja cijeli raspored.
Sažetak u pet rečenica
- Mars Pathfinder se 1997. stalno resetovao zbog inverzije prioriteta, a nestanak struje 2003. pogoršala je trka podataka koja je tiho zaustavila alarme: paralelne greške su rijetke, nepredvidive i opasne.
- Trka podataka nastaje kad više niti čita i mijenja isti podatak, kao brojač koji je istim kupcima dao isti broj narudžbe, a brava pretvara „pročitaj i upiši” u kritičnu sekciju.
- Paralelni kod se testira mnogo niti, provjerom svojstva koje ne zavisi od redoslijeda, ponavljanjem i barijerama, a jedan prolaz ne dokazuje mnogo, dok jedan pad dokazuje grešku.
- Zastoj nastaje kad niti čekaju jedna drugu u krug, sprečava se istim redoslijedom zaključavanja, a svako čekanje u testu mora imati vremensko ograničenje.
- Nestabilni testovi, uzrokovani paralelnošću, vremenom, redoslijedom, dijeljenim stanjem ili čekanjem na oko, ne smiju se ignorisati, nego istražiti, staviti u karantin uz zapisan defekt i popraviti.