Testiranje softvera · Poglavlje 12 · Sedmica 12

Testiranje paralelnih aplikacija

Dio 1 – Niti i trka podataka

Dio 2 – Zastoj i inverzija prioriteta

Dio 3 – Nestabilni testovi

Praksa – Niti, zastoji i nestabilni testovi

Materijale priredio Erkan Islamović

erkan.islamovic@eubd.edu.ba · erkan.islamovic@gmail.com

Šta učimo ovog semestra

Petnaest sedmica: svaka ima tri časa teorije i tri časa praktične nastave. Danas: poglavlje 12 (Sedmica 12, označeno). Isti primjer kroz knjigu: DeliveryApp, narudžba N-1001.

Sed.PoglavljeTeorija (3 časa)Praksa (3 časa)
1Zašto testiramoUvodne priče; greška, defekt, otkaz; sedam principa; DeliveryAppTabela očekivanih cijena rukom
2Proces i nivoiTest proces; nivoi i V-model; vrste; statičko testiranjeRecenzija zahtjeva DeliveryApp
3Manuelno testiranjeTest slučaj; izvještaj o defektu; istraživačko testiranjeTest slučajevi za web stranicu
4Automatizovano testiranjeŠta automatizovati; piramida testova; prvi testPrvi pytest
5Jedinično testiranjeAAA; fixture, parametri, izuzeci; TDDTestovi za dostavu i popust
6Bijela kutijaGraf toka; pokrivenost; ciklomatska složenostcoverage.py do 100 % grana
7Tok podatakaDefinicija i upotreba; anomalije; statička analiza, mutacijeruff, mypy, ručna mutacija
8Crna kutijaEkvivalencija i granice; tabela odlučivanja; stanjaLov na pet grešaka

Šta učimo ovog semestra

Sed.PoglavljeTeorija (3 časa)Praksa (3 časa)
9Model i integracijaModel; strategije integracije; APITestClient za API
10Sistemsko testiranjeSistemsko; performanse i sigurnost; prihvatno i BDDLocust, Gherkin
11OO testiranjeStanje objekta; nasljeđivanje; dvojniciNarudzba, unittest.mock
12Paralelne aplikacijeTrka podataka; zastoj; nestabilni testoviNiti, 1000 ponavljanja
13Web aplikacijeWeb i DOM; Playwright; Page Object, pristupačnostPlaywright za DeliveryApp
14Upravljanje testiranjemTest plan i rizik; metrike; CI/CDGitHub Actions
15Alati, AI, projekatPregled alata; AI u testiranju; ISTQBOdbrana projekta
N-1001 je 24,50 € · od 30,00 € dostava je besplatna

Literatura

  1. J. Popović: Priručnik za test inženjere, CET i Računarski fakultet, Beograd, 2023.
  2. Rakić-Skoković: Priručnik za testiranje softvera, Fakultet tehničkih nauka, Novi Sad, 2013.
  3. G. J. Myers: The Art of Software Testing, 2. izdanje, John Wiley & Sons, 2004.
  4. A. P. Mathur: Foundations of Software Testing, Addison-Wesley Professional, 2008.
Priča za početak · Sedmica 12

Priča za početak

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.

Zašto ovo poglavlje

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.

Riječi ovog poglavlja (1/2)

  • 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.

Riječi ovog poglavlja (2/2)

  • 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

ČasSadržajNa času
Teorija 112.1–12.3 Niti i dijeljeni podaci; trka podataka; bravaDva kupca, isti broj narudžbe
Teorija 212.4–12.6 Zastoj; inverzija prioriteta; kako se testira paralelni kodKuhinja i dostava čekaju jedna drugu
Teorija 312.7–12.9 Nestabilni testovi; uzroci; šta raditi s njimaTest koji pada svaki deseti put
Dio 1 · Sedmica 12

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:

  1. Redoslijed nije zadan. Operativni sistem sam odlučuje koja nit radi kad, i to se mijenja od pokretanja do pokretanja.
  2. Greške su rijetke. Pogrešan redoslijed se može desiti jednom u hiljadu ili milion pokretanja.
  3. Posmatranje mijenja ponašanje. Dodate li print da 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 – 12.1 Više stvari u isto vrijeme

  • 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):

12.2 Trka podataka: isti broj za dvije narudžbe

  1. nit A pročita 1000;
  2. sistem prebaci na nit B, koja takođe pročita 1000;
  3. nit A upiše 1001 i vrati N-1001;
  4. nit B upiše 1001 i vrati N-1001.
Slika 12.1 – Trka podataka: obje niti pročitaju 1000 prije nego što ijedna upiše
Slika 12.1 – Trka podataka: obje niti pročitaju 1000 prije nego što ijedna upiše

12.2 Trka podataka: isti broj za dvije narudžbe

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.

12.2 Trka podataka: isti broj za dvije narudžbe

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, funkciju nova_narudzba: BROJAC[0] += 1, pa broj = 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 – 12.2 Trka podataka: isti broj za dvije narudžbe

  • 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.

12.3 Brava i test s mnogo niti

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:

12.3 Brava i test s mnogo niti

  1. mnogo niti i mnogo operacija (8 × 200), da se poveća šansa sudara;
  2. provjera svojstva, a ne tačnog redoslijeda (ne znamo koja nit dobija koji broj, ali znamo da su svi različiti);
  3. 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 += 1 ima 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 – 12.3 Brava i test s mnogo niti

  • 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

  1. Šta je trka podataka? Greška u kojoj rezultat zavisi od slučajnog redoslijeda kojim niti pristupaju istom podatku.
  2. 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.
  3. Koje svojstvo provjerava test brojača s nitima? Da su svi dobijeni brojevi različiti i da ih ima tačno 1600.

Odgovori: tipka R ili klik na zamućeno polje.

Dio 2 · Sedmica 12

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:

12.4 Zastoj

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: zastoj

12.4 Zastoj

Dva 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 – 12.4 Zastoj

  • 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):

  1. zadatak niskog prioriteta (meteo podaci) zaključa bravu nad zajedničkom sabirnicom;
  2. zadatak visokog prioriteta (upravljanje sabirnicom) treba istu bravu, pa čeka;
  3. zadatak srednjeg prioriteta (komunikacija), kojem brava ne treba, prekine niski zadatak i radi dugo;
  4. niski zadatak ne može završiti i pustiti bravu, pa visoki čeka srednji, iako je važniji od njega;
  5. nadzorni brojač (watchdog) primijeti da visoki zadatak predugo nije završio i resetuje računar.
Slika 12.2 – Inverzija prioriteta na Pathfinderu i rješenje nasljeđivanjem prioriteta
Slika 12.2 – Inverzija prioriteta na Pathfinderu i rješenje nasljeđivanjem prioriteta

12.5 Inverzija prioriteta

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.

12.5 Inverzija prioriteta

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 – 12.5 Inverzija prioriteta

  • 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:

12.6 Kako se testira paralelni kod

TehnikaKakoU DeliveryApp
stres s nitimamnogo niti, mnogo operacija istovremeno8 niti × 200 brojeva
ponavljanjeisti test stotine ili hiljade putarange(20); dodatak pytest-repeat (--count=1000)
provjera svojstvaprovjerava se ono što mora važiti uvijek, ne tačan redoslijedsvi brojevi različiti
ubrzavanje prebacivanjaubaciti tačke u kojima sistem prebacuje nititime.sleep(0) između čitanja i upisa
barijereniti se sinhronizuju na tačno određenom mjestu, pa je scenarij ponovljivtest zastoja
vremenska ograničenjasvako čekanje ima rokacquire(timeout=0.5), pytest-timeout
statička analiza i recenzijatražiti dijeljene podatke bez brave čitanjem kodaBROJAC[0] += 1 u api.py
test opterećenjastotine pravih korisnika istovremenoLocust iz poglavlja 10, s provjerom jedinstvenih brojeva

12.6 Kako se testira paralelni kod

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 – 12.6 Kako se testira paralelni kod

  • 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

  1. 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.
  2. Kako je riješen problem Pathfindera? Uključenjem nasljeđivanja prioriteta za bravu, porukom poslanom sa Zemlje.
  3. 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.

Odgovori: tipka R ili klik na zamućeno polje.

Dio 3 · Sedmica 12

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:

12.7 Test koji ponekad pada

UzrokPrimjerLijek
paralelnostbrojač bez bravebrava; test s nitima koji se ponavlja
vrijemetest radnog vremena prolazi samo prije 23 hpatch trenutnog vremena (poglavlje 11)
redoslijed testovatest očekuje broj N-1001, ali neki raniji test je već napravio narudžbusvaki test sam pravi svoje podatke; fixture
dijeljeno stanjeglobalni rječnik NARUDZBE u api.py puni se iz testa u testizolacija: 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čajnosttest koristi random bez sjemenafiksno sjeme (poglavlje 9)
spoljni servisi i mrežatest zove pravi servis mapa koji ponekad kasnidvojnik u jediničnim testovima; ponovni pokušaj u sistemskim
decimalni brojevi i zaokruživanjepoređenje 0.1 + 0.2 == 0.3centi (poglavlje 5)

12.7 Test koji ponekad pada

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 sesiji

Pokrenut 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 – 12.7 Test koji ponekad pada

  • 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:SJEME ga ponavlja, -p no:randomly isključ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 – 12.8 Kako naći nestabilan test

  • 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:

  1. „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.
  2. „Obriši test.” Gubite i ono što je test dobro provjeravao.
  3. „Dodaj time.sleep(5).” Test postaje spor i i dalje nestabilan na sporijem računaru.

Dobar postupak:

  1. 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.
  2. 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.
  3. Popraviti uzrok: bravu u kodu, fixture umjesto dijeljenog stanja, patch umjesto pravog vremena, čekanje uslova umjesto sleep.
  4. Dokazati popravku ponavljanjem: test koji je ranije padao jednom u 50 pokretanja mora proći, recimo, 1000 puta zaredom.

12.9 Šta raditi s nestabilnim testom

Č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 – 12.9 Šta raditi s nestabilnim testom

  • 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

  1. Navedite tri uzroka nestabilnih testova. Npr. paralelnost, trenutno vrijeme, redoslijed testova (dijeljeno stanje), čekanje fiksnim sleep, slučajnost bez sjemena, mreža.
  2. Šta otkriva dodatak pytest-randomly? Testove koji zavise od drugih testova, jer svaki put izmiješa njihov redoslijed.
  3. Š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.

Odgovori: tipka R ili klik na zamućeno polje.

Praksa · Sedmica 12

Praktična nastava – Niti, zastoji i nestabilni testovi

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)

  1. U primjeri/paralelno pokrenite pytest -v -rx test_brojac.py. Zapišite rezultate.
  2. U Python konzoli pokrenite podijeli_brojeve(Brojac()) pet puta i zapišite broj jedinstvenih brojeva. Zatim pet puta sa BrojacSaBravom().
  3. U svojoj kopiji brojac.py obrišite red time.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.
  4. 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)

  1. Napišite test koji pokrene 8 niti, a svaka preko TestClient napravi 50 narudžbi (POST /v1/narudzbe). Provjerite da su svi brojevi različiti. Ponovite 20 puta.
  2. Ispravite api.py u svojoj kopiji bravom oko povećanja i čitanja brojača. Ponovite test.
  3. Zapišite nalaz kao izvještaj o defektu: naslov, koraci, očekivano, dobijeno, ozbiljnost.

Korak 3 – Zastoj (30 minuta)

  1. 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.
  2. 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)

  1. U primjeri/deliveryapp pokrenite pytest tests tri puta (pytest-randomly svaki put miješa redoslijed i na vrhu ispiše sjeme), i pytest --count=20 tests/test_api.py. Da li neki test pada samo ponekad?
  2. Dodajte namjerno nestabilan test test_prva_narudzba_ima_broj_1001 iz 12.7 u svoju kopiju test_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.
  3. 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žbe za studenta

Rješenja su sakrivena – tipka R ili klik na okvir.

Vježba za studenta 1 – trka.

Zadatak

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?

Vježba za studenta 1 – trka. – rješenje

Rješenje (R)

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.

Zadatak

Funkcija istovremeno (8 niti) dodaje stavke u korpu. Koje svojstvo test treba provjeriti?

Vježba za studenta 2 – svojstvo. – rješenje

Rješenje (R)

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.

Zadatak

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?

Vježba za studenta 3 – zastoj. – rješenje

Rješenje (R)

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.

Zadatak

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?

Vježba za studenta 4 – nestabilan test. – rješenje

Rješenje (R)

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.

Zadatak

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?

Vježba za studenta 5 – Pathfinder. – rješenje

Rješenje (R)

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Pitanja za ponavljanje

  1. Šta se desilo s Mars Pathfinderom i kako je problem riješen?
  2. Kakvu je ulogu trka podataka imala u nestanku struje 2003. godine?
  3. Šta je nit i zašto dijeljeni podaci prave probleme?
  4. Objasnite na primjeru brojača narudžbi kako nastaje trka podataka.
  5. Zašto test brojača bez brave bez time.sleep(0) u 200 pokretanja nije pao, a kod ipak nije ispravan?
  6. Šta je brava i kritična sekcija?
  7. Koje tri tehnike koristi test test_sa_bravom_svi_brojevi_razliciti?
  8. Šta je zastoj i koja su četiri uslova za njega?
  9. Kako test otkriva zastoj, a da ne visi zauvijek?
  10. Objasnite inverziju prioriteta i nasljeđivanje prioriteta.
  11. Šta je nestabilan test i zašto je opasan za tim?
  12. Navedite pet uzroka nestabilnih testova s primjerima.
  13. Koji alati pomažu da se nađu nestabilni testovi?
  14. Šta treba, a šta ne treba raditi s nestabilnim testom?