Testiranje softvera · Poglavlje 9 · Sedmica 9

Testiranje zasnovano na modelu i integracija

Dio 1 – Model kao izvor testova

Dio 2 – Integracija

Dio 3 – API i ugovor

Praksa – Model, integracija i API DeliveryApp

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 9 (Sedmica 9, 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 9

Priča za početak

Priča za početak

Ujutro 20. decembra 2019. raketa Atlas V poletjela je s Floride noseći novu Boeingovu kapsulu Starliner, koja je trebala, bez posade, prvi put doletjeti do Međunarodne svemirske stanice. Lansiranje je bilo savršeno. Ali kad se kapsula odvojila od rakete, nije upalila motore da se podigne na pravu orbitu. Njen tajmer misije, sat po kojem kapsula zna u kojoj je fazi leta, pokazivao je pogrešno vrijeme, pomjereno za oko 11 sati. Tajmer je vrijeme uzeo od rakete u pogrešnom trenutku, još prije završnog odbrojavanja. Kapsula je „mislila” da je u drugom dijelu leta i trošila gorivo na pogrešne manevre. Ostalo joj ga je premalo, pa je spajanje sa stanicom otkazano, a kapsula se vratila na Zemlju poslije dva dana. Za vrijeme leta inženjeri su našli i drugu grešku u softveru, koja je mogla izazvati sudar dijelova kapsule pri povratku; ispravili su je sa Zemlje, na vrijeme.

Istraga NASA-e i Boeinga otkrila je zašto testovi na zemlji nisu našli nijednu od dvije greške. Simulacija cijelog leta trajala bi više od 25 sati, pa je Boeing testove podijelio na dijelove: jedan dio je testirao let do jedne tačke, drugi od te tačke dalje. Svaki dio je prošao. Ali cijeli let, od lansiranja do spajanja, nikad nije testiran u jednom komadu, a greška tajmera živjela je baš na prelazu između dijelova. Druga greška je promakla jer je simulator koji se koristio u testovima bio stari kod iz drugih projekata, bez tačnog rasporeda Starlinerovih motora. Komisija je dala 61 preporuku, a prva je bila jasna: testirati od kraja do kraja, u cjelini. Ovo poglavlje govori o tome kako se dijelovi sistema testiraju zajedno.

Slika 9.1 – Starliner 2019: svaki dio je testiran, prelaz između dijelova nije
Slika 9.1 – Starliner 2019: svaki dio je testiran, prelaz između dijelova nije

Zašto ovo poglavlje

Do sada smo testirali jedinice: funkciju cijena_dostave, funkciju popust, klasu Narudzba, svaku zasebno. U poglavlju 5 smo rekli da jedinični testovi ne vide saradnju dijelova, a u poglavlju 2 da je Heathrow pao na šavu između sistema. Sada učimo kako se testira taj šav. Prvo testiranje zasnovano na modelu: kako od malog, jasnog modela ponašanja (dijagram stanja iz poglavlja 8) napraviti stotine testova automatski. Zatim integraciju: strategije spajanja dijelova i test dvojnike koji glume dijelove kojih još nema. Na kraju testiramo API DeliveryApp, vrata kroz koja web stranica i mobilna aplikacija razgovaraju sa serverom, i ugovor koji ta vrata moraju poštovati.

Riječi ovog poglavlja (1/2)

  • Model – pojednostavljen opis ponašanja sistema (dijagram stanja, tabela pravila) iz kojeg se mogu izvesti testovi.
  • Testiranje zasnovano na modelu (model-based testing, MBT) – testovi se automatski prave iz modela, a rezultat sistema poredi se s onim što model predviđa.
  • Integraciono testiranje – testiranje saradnje dva ili više dijelova: da li se pozivaju pravilno, s pravim podacima, u pravim jedinicama.
  • Šav (interface, sučelje) – mjesto gdje jedan dio sistema predaje podatke drugom.
  • Stub – zamjena za donji dio sistema koja vraća unaprijed zadan odgovor.
  • Drajver (driver) – zamjena za gornji dio sistema koja poziva dio koji testiramo; u pytestu je to sam test.

Riječi ovog poglavlja (2/2)

  • Test dvojnik (test double) – opšti naziv za svaku zamjenu u testu: dummy, stub, špijun, mock, fake.
  • API (Application Programming Interface) – dogovoreni način na koji jedan program traži usluge od drugog, npr. HTTP zahtjev POST /v1/cijena.
  • Statusni kod – broj u HTTP odgovoru koji kaže kako je zahtjev prošao: 200 u redu, 201 napravljeno, 404 ne postoji, 409 konflikt, 422 neispravni podaci.
  • Ugovor (contract) – dogovor između dva dijela o tome šta se šalje i šta se vraća: imena polja, tipovi, jedinice, statusni kodovi.
  • Test od kraja do kraja (end-to-end) – test koji prolazi kroz cijeli sistem, od ulaza korisnika do konačnog rezultata.

Ciljevi poglavlja

Nakon poglavlja možete:

objasniti šta je model i kako se iz njega prave testovi; napisati test koji poredi implementaciju s modelom kroz mnogo slučajnih nizova radnji; objasniti zašto su ponovljivi slučajni testovi važni; opisati strategije integracije (veliki prasak, odozgo nadolje, odozdo nagore, sendvič); razlikovati stub, drajver, špijuna i fake; napraviti integracioni test sa stubom i sa „pravim” servisom; prepoznati grešku na šavu između dva dijela; testirati HTTP API kroz FastAPI TestClient; provjeriti statusne kodove i ugovor API-ja.

Plan poglavlja – Sedmica 9: tri časa teorije i tri časa praktične nastave

ČasSadržajNa času
Teorija 19.1–9.3 Model kao izvor testova; slučajni nizovi radnji200 nizova radnji za jednu narudžbu
Teorija 29.4–9.6 Strategije integracije; test dvojnici; greška na šavuMetri ili kilometri?
Teorija 39.7–9.9 API, HTTP i statusni kodovi; TestClient; ugovorPOST /v1/cijena za N-1001
Dio 1 · Sedmica 9

Dio 1 – Model kao izvor testova

9.1 Šta je model

Model je pojednostavljen opis onoga što sistem treba da radi. Nije sam sistem, nego „mapa” sistema: dovoljno mala da se razumije na prvi pogled, a dovoljno tačna da se iz nje vidi šta je ispravno. Već smo ih koristili:

  • dijagram stanja narudžbe iz poglavlja 8 (sedam stanja, sedam prelaza);
  • tabelu odlučivanja za dostavu i popust iz poglavlja 8;
  • graf toka kontrole iz poglavlja 6 (model koda, a ne pravila);
  • dijagram aktivnosti ili tok ekrana: korpa → adresa → plaćanje → potvrda.

Slika iz života: maketa zgrade kod arhitekte. Maketa nije zgrada, u njoj se ne može stanovati, ali iz nje se odmah vidi da li su vrata na pravom mjestu. Ako se zgrada i maketa razlikuju, jedno od njih ne valja.

Testiranje zasnovano na modelu (MBT) koristi model na dva načina:

9.1 Šta je model

  1. kao generator testova: program prolazi kroz model i pravi nizove radnji (npr. „prihvati, počni pripremu, otkaži”);
  2. kao proročište (test oracle): za svaki korak model kaže šta treba da se desi, pa se rezultat sistema automatski uporedi s modelom.

Druga uloga je ključna. Najteži dio svakog testa je očekivani rezultat: do sada smo ga računali rukom za svaki test. Kad je model jednostavan i tačan, on sam izračuna očekivani rezultat za hiljade testova.

Ukratko – 9.1 Šta je model

  • Model je pojednostavljen opis ponašanja: dijagram stanja, tabela pravila, tok ekrana.
  • Testiranje zasnovano na modelu automatski pravi testove iz modela.
  • Model služi i kao proročište: za svaki korak kaže šta je ispravan rezultat.
  • Ako se sistem i model razlikuju, greška je u jednom od njih, i to treba istražiti.

9.2 Model narudžbe u kodu

Dijagram stanja iz poglavlja 8 možemo zapisati kao mali rječnik u Pythonu: za svaki par (stanje, radnja) piše koje je novo stanje. Fajl tests/test_model.py:

# Model iz specifikacije: (stanje, radnja) -> novo stanje
MODEL = {
    ("NOVA", "prihvati"): "PRIHVAĆENA",
    ("NOVA", "odbij"): "ODBIJENA",
    ("NOVA", "otkaži"): "OTKAZANA",
    ("PRIHVAĆENA", "počni pripremu"): "U_PRIPREMI",
    ("PRIHVAĆENA", "otkaži"): "OTKAZANA",
    ("U_PRIPREMI", "preuzmi"): "NA_PUTU",
    ("NA_PUTU", "isporuči"): "ISPORUČENA",
}

Ako para nema u rječniku, radnja nije dozvoljena. Model je toliko jednostavan da ga može provjeriti i vlasnik proizvoda koji ne zna programirati: sedam redova, sedam strelica na dijagramu.

Zatim test koji napravi slučajan niz od osam radnji i u svakom koraku poredi klasu Narudzba s modelom:

9.2 Model narudžbe u kodu

@pytest.mark.parametrize("sjeme", range(200))
def test_slucajni_niz_radnji(sjeme):
    rng = random.Random(sjeme)           # isto sjeme -> isti niz: test je ponovljiv
    n, stanje_modela = Narudzba("N-1001"), "NOVA"
    for _ in range(8):
        radnja = rng.choice(list(RADNJE))
        ocekivano = MODEL.get((stanje_modela, radnja))
        if ocekivano is None:
            with pytest.raises(ValueError):
                n.promijeni(RADNJE[radnja])
        else:
            n.promijeni(RADNJE[radnja])
            stanje_modela = ocekivano
        assert n.stanje == stanje_modela, f"poslije radnje {radnja!r}"

9.2 Model narudžbe u kodu

Šta se dešava u svakom koraku:

  1. izabere se slučajna radnja (npr. „otkaži”);
  2. model kaže da li je dozvoljena i u koje stanje vodi;
  3. ako nije dozvoljena, Narudzba mora javiti ValueError;
  4. ako jeste, Narudzba mora preći u isto stanje kao model.

Parametar sjeme (seed) od 0 do 199 daje 200 različitih nizova, ukupno 1600 koraka. Nad ispravnom verzijom:

pytest -q tests/test_model.py
200 passed in 0.13s

9.2 Model narudžbe u kodu

Nad prvom verzijom (DeliveryApp=v0), u kojoj se narudžba u pripremi može otkazati (D-04):

FAILED tests/test_model.py::test_slucajni_niz_radnji[6] - Failed: DID NOT RAISE ...
...
11 failed, 189 passed in 0.18s

Jedanaest od dvjesto nizova slučajno je došlo do stanja U_PRIPREMI i onda probalo „otkaži”. Nismo morali znati gdje je greška: model ju je našao sam.

Ukratko – 9.2 Model narudžbe u kodu

  • Model narudžbe je rječnik od sedam redova koji za par (stanje, radnja) daje novo stanje.
  • Test pravi slučajne nizove radnji i u svakom koraku poredi implementaciju s modelom.
  • 200 nizova od po osam radnji prolazi nad ispravnom verzijom za manje od pola sekunde.
  • Nad prvom verzijom 11 nizova pada i tako otkriva D-04, bez ručno napisanog testa za taj slučaj.

9.3 Slučajno, ali ponovljivo

Slučajni testovi imaju jednu opasnost: ako test padne danas, a sutra ga pokrenemo s drugim slučajnim brojevima, možda neće pasti. Tada ne možemo ni naći grešku ni provjeriti ispravku. Rješenje je sjeme (seed): generator slučajnih brojeva pokrenut istim sjemenom daje uvijek isti niz. Test test_slucajni_niz_radnji[6] uvijek pravi isti niz radnji, na svakom računaru, i uvijek pada nad v0 na istom mjestu. To je prvo svojstvo dobrog testa iz poglavlja 4: ponovljivost.

Testiranje zasnovano na modelu ima i svoju cijenu:

  • model mora biti tačan: ako je pogrešan, test će „hvatati” ispravan kod; zato se model prepisuje iz specifikacije i recenzira;
  • model mora biti jednostavniji od sistema: ako je model složen kao kod, samo smo napisali program dva puta;
  • ne vidi sve: naš model zna stanja, ali ne zna cijene, vrijeme, korisnike.
Za radoznale

Za Python postoji biblioteka Hypothesis, koja ovu ideju razvija dalje. Umjesto da sami pišemo petlju sa slučajnim radnjama, opišemo pravila („ovo je stanje, ovo su radnje, ovo mora uvijek važiti”), a Hypothesis sam pravi nizove. Kad nađe niz koji obara test, skraćuje ga do najkraćeg mogućeg, npr. do „prihvati, počni pripremu, otkaži”, pa programer odmah vidi suštinu greške. Ovaj način se zove testiranje svojstava (property-based testing).

Ukratko – 9.3 Slučajno, ali ponovljivo

  • Slučajni testovi moraju biti ponovljivi, a to se postiže fiksnim sjemenom generatora.
  • Isto sjeme daje isti niz radnji na svakom računaru, pa se pad može ponoviti i ispravka provjeriti.
  • Model mora biti tačan i mnogo jednostavniji od sistema.
  • Biblioteka Hypothesis automatski pravi nizove i skraćuje onaj koji obara test.

Provjeri se

  1. Koje su dvije uloge modela u testiranju zasnovanom na modelu? Generator testova i proročište koje kaže ispravan rezultat.
  2. Zašto se slučajnom generatoru daje sjeme? Da isti test uvijek pravi isti niz, pa je pad ponovljiv.
  3. Koju grešku prve verzije model narudžbe hvata? D-04: otkazivanje iz stanja U_PRIPREMI.

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

Dio 2 · Sedmica 9

Dio 2 – Integracija

9.4 Strategije integracije

DeliveryApp nije jedna funkcija. Kad kupac naruči, dešava se lanac:

  1. servis mapa izračuna udaljenost adrese od restorana;
  2. pravila DeliveryApp izračunaju ukupno (ukupno, koja zove cijena_dostave i popust);
  3. servis plaćanja (banka) naplati iznos;
  4. napravi se nova narudžba u stanju NOVA.

Taj lanac je u fajlu primjeri/deliveryapp/narucivanje.py:

9.4 Strategije integracije

class Narucivanje:
    def __init__(self, mape, placanje):
        self.mape = mape            # ima udaljenost_m(adresa) -> metri
        self.placanje = placanje    # ima naplati(iznos_centi) -> broj transakcije ili None
        self.brojac = 1000

    def naruci(self, stavke, adresa, kod=None):
        """Izračuna iznos, naplati ga i vrati novu narudžbu."""
        km = self.mape.udaljenost_m(adresa) / 1000
        iznos = ukupno(stavke, km, kod)
        transakcija = self.placanje.naplati(iznos)
        if transakcija is None:
            raise RuntimeError("plaćanje odbijeno")
        self.brojac += 1
        n = Narudzba(f"N-{self.brojac}")
        return n, iznos, transakcija

9.4 Strategije integracije

Kojim redom spajati i testirati dijelove? Postoje četiri strategije:

9.4 Strategije integracije

StrategijaKakoPrednostMana
veliki prasak (big bang)sve se spoji odjednom i testiranema dodatnog kodakad padne, ne zna se gdje; greške se nađu kasno
odozgo nadolje (top-down)prvo gornji dio (Narucivanje), a donji se zamijene stubovimarano se vidi cijeli toktreba pisati stubove
odozdo nagore (bottom-up)prvo donji dijelovi (cijena_dostave, popust), a gornji glumi drajverdonji dijelovi su temeljno testiranicijeli tok se vidi tek na kraju
sendvičodozgo i odozdo u isto vrijeme, sastaju se u sredinibrzo, više timova radi paralelnosloženije za organizovati
Slika 9.2 – Odozgo nadolje (stubovi umjesto donjih dijelova) i odozdo nagore (drajver umjesto gornjeg)
Slika 9.2 – Odozgo nadolje (stubovi umjesto donjih dijelova) i odozdo nagore (drajver umjesto gornjeg)

9.4 Strategije integracije

U DeliveryApp smo do sada radili odozdo nagore: u poglavljima 4–8 testirali smo cijena_dostave, popust i Narudzba, a drajver je bio sam pytest test, koji poziva funkciju kao što bi je zvao gornji dio. Sada idemo korak gore: Narucivanje koristi te funkcije i dva spoljna servisa. Servisi mapa i banke nisu naši, a u testu ih ne želimo zvati (spori su, naplaćuju, zahtijevaju internet). Zato ih zamjenjujemo.

Starliner je, na neki način, primjer velikog praska bez praska: dijelovi su testirani odvojeno, a cjelina nikad. Strategija integracije mora uvijek završiti testom od kraja do kraja.

Ukratko – 9.4 Strategije integracije

  • Integraciono testiranje provjerava saradnju dijelova, a strategija određuje redoslijed spajanja.
  • Veliki prasak spaja sve odjednom i otežava nalaženje greške.
  • Odozgo nadolje koristi stubove umjesto donjih dijelova, a odozdo nagore drajvere umjesto gornjih.
  • Svaka strategija mora završiti testom cijelog toka, od kraja do kraja.

9.5 Test dvojnici

Test dvojnik je objekat koji u testu glumi pravi dio sistema. Ime dolazi iz filma: kad je scena opasna, umjesto glumca igra dubler. Postoji pet vrsta, i dobro je znati razliku (slika 9.3):

Slika 9.3 – Pet vrsta test dvojnika
Slika 9.3 – Pet vrsta test dvojnika

9.5 Test dvojnici

VrstaŠta radiU DeliveryApp
dummysamo popunjava parametar; nikad se ne koristiobjekat poslan funkciji koja ga ne dira
stubvraća unaprijed zadan odgovorStubMape(4200): svaka adresa je 4200 m
špijun (spy)radi kao stub, ali zapamti kako je pozvanSpyPlacanje: pamti iznose u listi pozivi
mockunaprijed zna koje pozive mora dobiti i sam provjeripoglavlje 11 (unittest.mock)
fakezaista radi, ali pojednostavljenoPlacanjeServis: lista u memoriji umjesto prave banke

Dvojnici u tests/test_narucivanje.py su obične male klase, bez ikakve biblioteke:

9.5 Test dvojnici

class StubMape:
    """Stub: uvijek vrati istu udaljenost, u METRIMA, kao što kaže ugovor servisa mapa."""

    def __init__(self, metri):
        self.metri = metri

    def udaljenost_m(self, adresa):
        return self.metri


class SpyPlacanje:
    """Špijun: odobri svaku naplatu i zapamti s kojim iznosom je pozvan."""

    def __init__(self):
        self.pozivi = []

    def naplati(self, iznos_centi):
        self.pozivi.append(iznos_centi)
        return "T-TEST"

9.5 Test dvojnici

I testovi, svaki s očekivanim rezultatom iz pravila:

9.5 Test dvojnici

def test_naplacuje_se_ukupan_iznos():
    placanje = SpyPlacanje()
    n, iznos, _ = Narucivanje(StubMape(4200), placanje).naruci(N1001, "bilo koja adresa")
    assert iznos == 2450                 # 21,50 € + 3,00 € dostave (4,2 km)
    assert placanje.pozivi == [2450]     # banka je pozvana jednom, s tačnim iznosom
    assert n.stanje == "NOVA"


def test_izvan_zone_banka_se_ne_poziva():
    placanje = SpyPlacanje()
    with pytest.raises(ValueError):
        Narucivanje(StubMape(11000), placanje).naruci(N1001, "bilo koja adresa")
    assert placanje.pozivi == []         # novac se ne skida za narudžbu koja ne može biti dostavljena

9.5 Test dvojnici

Drugi test pokazuje zašto je špijun koristan. Nije dovoljno da naruci javi grešku za 11 km. Važno je i šta se nije desilo: banka nije pozvana. Da je kod prvo naplatio pa onda računao dostavu, kupac bi platio narudžbu koja nikad neće stići. Takvu grešku vidi samo test koji gleda saradnju dva dijela.

Česta zabluda

„Što više dvojnika, to bolje: testovi su brži.” Dvojnik je pretpostavka o tome kako se pravi dio ponaša. Ako je pretpostavka pogrešna, test prolazi, a sistem ne radi. Test sa samim dvojnicima nije integracioni test, nego jedinični test u kostimu. Zato uz testove s dvojnicima uvijek treba bar nekoliko testova s pravim (ili vjerno lažnim) dijelovima.

Ukratko – 9.5 Test dvojnici

  • Test dvojnik glumi pravi dio sistema u testu, kao dubler u filmu.
  • Stub vraća zadan odgovor, špijun pamti pozive, mock sam provjerava očekivane pozive, a fake radi pojednostavljeno.
  • Špijun pokazuje i ono što se nije smjelo desiti, npr. naplatu narudžbe izvan zone.
  • Dvojnik je pretpostavka o pravom dijelu, pa testovi s dvojnicima ne zamjenjuju integraciju s pravim dijelovima.

9.6 Greška na šavu: metri ili kilometri?

Servis mapa ima ugovor, zapisan u servisi.py: „udaljenost_m(adresa) vraća udaljenost u metrima, cijeli broj”. Pravila DeliveryApp (ukupno, cijena_dostave) rade u kilometrima. Neko mora pretvoriti metre u kilometre, i to je posao klase Narucivanje: km = self.mape.udaljenost_m(adresa) / 1000.

U prvoj verziji (narucivanje_v0.py) programer je to zaboravio:

        km = self.mape.udaljenost_m(adresa)          # I-01: metri poslani kao kilometri

Šta se desi? Adresa „Lipa 12” je 4200 m daleko. Funkcija cijena_dostave dobije 4200 km i odbije narudžbu: „udaljenost izvan zone dostave”. Svaka narudžba, osim onih na manje od 10 metara, bude odbijena.

9.6 Greška na šavu: metri ili kilometri?

Ovo je klasična greška na šavu (integraciona greška): svaki dio je ispravan za sebe. MapeServis vraća metre, kako kaže ugovor. cijena_dostave računa kilometre, kako kaže PP-2. Jedinični testovi oba dijela prolaze. Greška je u predaji podataka između njih, kao kod Mars Climate Orbitera iz poglavlja 1 (funte i njutni), i kao kod Starlinera (tajmer uzet u pogrešnom trenutku).

Testovi nad prvom verzijom:

pytest -q tests/test_narucivanje.py          # sa DeliveryApp=v0
FAILED tests/test_narucivanje.py::test_naplacuje_se_ukupan_iznos - ValueError...
FAILED tests/test_narucivanje.py::test_odbijeno_placanje_ne_pravi_narudzbu - ...
FAILED tests/test_narucivanje.py::test_n1001_s_pravim_servisima - ValueError:...
FAILED tests/test_narucivanje.py::test_n1002_s_pravim_servisima - ValueError:...
4 failed, 3 passed in 0.16s

Svaki pad je ValueError: udaljenost izvan zone dostave. Primijetite: stub StubMape(4200) je poštovao ugovor (vraća metre), pa je i on uhvatio grešku. Da je programer sam napisao stub koji vraća 4.2, jer je mislio da servis vraća kilometre, njegov test s dvojnikom bi prošao, a greška bi otišla u produkciju. Zato stubove treba praviti iz ugovora, a ne iz onoga što misli kod koji se testira.

9.6 Greška na šavu: metri ili kilometri?

Najbolja zaštita su ugovorni testovi (contract tests): mali testovi koji provjeravaju da pravi servis zaista radi ono što ugovor kaže.

def test_ugovor_mape_vraca_metre_kao_cijeli_broj():
    d = MapeServis().udaljenost_m("Lipa 12")
    assert isinstance(d, int) and d == 4200


def test_ugovor_mape_nepoznata_adresa():
    with pytest.raises(KeyError):
        MapeServis().udaljenost_m("Nepostojeća 99")

Ako tim koji održava servis mapa jednog dana promijeni odgovor u kilometre, ugovorni test padne odmah, a ne tek kad kupci počnu dobijati pogrešne cijene.

9.6 Greška na šavu: metri ili kilometri?

Iz stvarnog svijeta

Velike firme s mnogo servisa (stotine malih programa koji se međusobno zovu) koriste alate za ugovorno testiranje, npr. Pact. Tim koji koristi servis zapiše šta očekuje, a tim koji održava servis automatski provjerava da to i dalje isporučuje, pri svakoj izmjeni.

Ukratko – 9.6 Greška na šavu: metri ili kilometri?

  • Greška na šavu nastaje kad su oba dijela ispravna, ali se podaci pogrešno predaju između njih.
  • U prvoj verziji naručivanja metri su poslani kao kilometri (I-01), pa je svaka narudžba bila izvan zone.
  • Stub mora poštovati ugovor pravog servisa, inače i on dijeli pogrešnu pretpostavku koda.
  • Ugovorni testovi provjeravaju da pravi servis i dalje radi ono što ugovor kaže.

Provjeri se

  1. Šta je drajver u integraciji odozdo nagore? Zamjena za gornji dio koja poziva dio koji testiramo; u pytestu je to sam test.
  2. Koja je razlika između stuba i špijuna? Stub samo vraća zadan odgovor; špijun uz to pamti kako je pozvan, pa test može provjeriti pozive.
  3. Zašto jedinični testovi nisu našli grešku I-01? Jer su oba dijela ispravna za sebe; greška je u predaji podataka između njih.

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

Dio 3 · Sedmica 9

Dio 3 – API i ugovor

9.7 HTTP API i statusni kodovi

Web stranica i mobilna aplikacija DeliveryApp ne zovu Python funkcije direktno. One šalju HTTP zahtjeve serveru, a server odgovara. Skup tih dogovorenih zahtjeva je API. DeliveryApp ima mali API u fajlu primjeri/deliveryapp/api.py, napravljen alatom FastAPI:

9.7 HTTP API i statusni kodovi

Metoda i putanjaŠta radiUspješan odgovor
GET /v1/jelajelovnik (opciono ?restoran=R-2)200 i lista jela
POST /v1/cijenaizračuna cijenu bez pravljenja narudžbe200 i račun
POST /v1/narudzbenapravi narudžbu201 i broj narudžbe
GET /v1/narudzbe/{broj}stanje i istorija narudžbe200
PATCH /v1/narudzbe/{broj}/stanjepromijeni stanje200; 409 ako prelaz nije dozvoljen

Zahtjev i odgovor su u formatu JSON. Primjer za N-1001:

POST /v1/cijena
{"kupac": "K-01", "udaljenost_km": 4.2,
 "stavke": [{"jelo_id": 1, "kolicina": 2}, {"jelo_id": 3, "kolicina": 1}]}

9.7 HTTP API i statusni kodovi

200 OK
{"artikli": 2150, "popust": 0, "dostava": 300, "ukupno": 2450, "valuta": "EUR", "jedinica": "cent"}

Primijetite dvije stvari. Klijent šalje samo jela i količine, ne i cijenu: server sam računa (komentar u kodu kaže: „ne vjeruje broju koji bi poslao klijent”, pouka Heartbleeda iz poglavlja 7). I odgovor kaže jedinicu: iznosi su u centima, u eurima. Pouka greške I-01: jedinica je dio ugovora.

Statusni kod je prvi broj u odgovoru i kaže kako je prošlo:

9.7 HTTP API i statusni kodovi

KodZnačenjeKada u DeliveryApp
200 OKuspjehcijena izračunata, stanje promijenjeno
201 Creatednešto je napravljenonova narudžba
404 Not Foundne postojijelo 99, narudžba N-9999
409 Conflictsukob sa stanjemotkazivanje narudžbe u pripremi (PP-6)
422 Unprocessablepodaci nisu ispravni11 km (PP-3), količina 0

Tester API-ja provjerava oba: statusni kod i sadržaj. Odgovor 200 s pogrešnim iznosom je greška; i odgovor 500 (Internal Server Error, server je pao) umjesto 422 je greška, čak i kad je narudžba ispravno odbijena.

Ukratko – 9.7 HTTP API i statusni kodovi

  • API je skup dogovorenih HTTP zahtjeva kroz koje web stranica i aplikacija razgovaraju sa serverom.
  • Zahtjevi i odgovori DeliveryApp su u formatu JSON, a iznosi su u centima, što piše i u odgovoru.
  • Server sam računa cijenu i ne vjeruje brojevima koje šalje klijent.
  • Statusni kod kaže ishod: 200, 201, 404, 409, 422; test provjerava i kod i sadržaj.

9.8 TestClient: API bez servera

Da bismo testirali API, ne moramo pokretati pravi server ni otvarati mrežu. FastAPI ima TestClient, koji šalje zahtjeve direktno aplikaciji, u istom procesu. Fajl tests/test_api.py (treba pip install fastapi httpx2):

9.8 TestClient: API bez servera

from fastapi.testclient import TestClient

from api import app

klijent = TestClient(app)

N1001 = {"kupac": "K-01", "udaljenost_km": 4.2,
         "stavke": [{"jelo_id": 1, "kolicina": 2}, {"jelo_id": 3, "kolicina": 1}]}


def test_cijena_n1001():
    r = klijent.post("/v1/cijena", json=N1001)
    assert r.status_code == 200
    assert r.json() == {"artikli": 2150, "popust": 0, "dostava": 300, "ukupno": 2450,
                        "valuta": "EUR", "jedinica": "cent"}


def test_izvan_zone_422():
    r = klijent.post("/v1/cijena", json={**N1001, "udaljenost_km": 11})
    assert r.status_code == 422
    assert r.json()["detail"] == "udaljenost izvan zone dostave"


def test_kolicina_nula_422():
    r = klijent.post("/v1/cijena", json={**N1001, "stavke": [{"jelo_id": 1, "kolicina": 0}]})
    assert r.status_code == 422          # odbija ga model podataka (Field(gt=0)), prije našeg koda

9.8 TestClient: API bez servera

Zapis {**N1001, "udaljenost_km": 11} znači „isto kao N-1001, samo s 11 km”: tako svaki test mijenja tačno jedan ulaz (nevažeće klase jedna po jedna, poglavlje 8).

Test životnog ciklusa koristi fixture koji preko API-ja napravi novu narudžbu:

9.8 TestClient: API bez servera

@pytest.fixture
def broj():
    """Nova narudžba N-1001 preko API-ja; vraća njen broj."""
    r = klijent.post("/v1/narudzbe", json=N1001)
    assert r.status_code == 201
    return r.json()["broj"]


def test_otkazivanje_u_pripremi_409(broj):
    for s in ("PRIHVAĆENA", "U_PRIPREMI"):
        klijent.patch(f"/v1/narudzbe/{broj}/stanje", json={"stanje": s})
    r = klijent.patch(f"/v1/narudzbe/{broj}/stanje", json={"stanje": "OTKAZANA"})
    assert r.status_code == 409          # PP-6: konflikt sa stanjem narudžbe
    assert klijent.get(f"/v1/narudzbe/{broj}").json()["stanje"] == "U_PRIPREMI"

9.8 TestClient: API bez servera

Rezultat nad ispravnom verzijom je 12 prolaza. Nad prvom verzijom (DeliveryApp=v0):

FAILED tests/test_api.py::test_cijena_n1001 - AssertionError: assert {'artikl...
FAILED tests/test_api.py::test_cijena_n1001_student10 - assert 2400 == 2235
FAILED tests/test_api.py::test_nova_narudzba_je_nova - assert 2400 == 2450
FAILED tests/test_api.py::test_otkazivanje_u_pripremi_409 - assert 200 == 409
4 failed, 8 passed in 0.40s

Iste greške (D-02, D-03, D-04) vidljive su i kroz API: ono što je pogrešno u pravilu, pogrešno je i na vratima sistema. Ali API test provjerava i ono što jedinični test ne vidi: da li server za grešku vraća pravi statusni kod, da li JSON ima prava imena polja, da li PATCH zaista mijenja stanje koje kasnije vraća GET.

Na času

Pokrenite server (uvicorn api:app --reload u folderu primjeri/deliveryapp) i otvorite http://127.0.0.1:8000/docs. FastAPI je sam napravio stranicu na kojoj se svaki zahtjev može isprobati dugmetom „Try it out”. Pošaljite N-1001 sa 11 km. Koji statusni kod i koju poruku dobijate?

Ukratko – 9.8 TestClient: API bez servera

  • TestClient šalje HTTP zahtjeve aplikaciji direktno, bez pokretanja servera i mreže.
  • Svaki API test provjerava statusni kod i sadržaj odgovora.
  • Fixture preko API-ja pravi novu narudžbu, pa svaki test počinje od čistog stanja.
  • Nad prvom verzijom API testovi otkrivaju D-02, D-03 i D-04 i provjeravaju statusne kodove.

9.9 Ugovor API-ja

Web stranicu DeliveryApp pravi jedan tim, mobilnu aplikaciju drugi, a server treći. Svi se oslanjaju na ugovor: koje putanje postoje, koja polja zahtjev ima, šta se vraća. Ako tim servera preimenuje polje ukupno u total, server radi savršeno, njegovi testovi prolaze, a mobilna aplikacija prikazuje prazan iznos.

FastAPI sam piše ugovor u standardnom formatu OpenAPI, na adresi /openapi.json. Test može provjeriti da putanje na koje se klijenti oslanjaju i dalje postoje:

def test_ugovor_openapi():
    """Ugovor: putanje koje klijenti (web stranica, mobilna aplikacija) koriste moraju postojati."""
    putanje = klijent.get("/openapi.json").json()["paths"]
    for p in ["/v1/jela", "/v1/cijena", "/v1/narudzbe", "/v1/narudzbe/{broj}", "/v1/narudzbe/{broj}/stanje"]:
        assert p in putanje

Tri pravila za ugovore, koja i DeliveryApp poštuje:

9.9 Ugovor API-ja

  1. Verzija u putanji (/v1/). Kad treba velika promjena, napravi se /v2/, a /v1/ radi dok svi klijenti ne pređu.
  2. Dodavati smijete, mijenjati i brisati ne. Novo polje (napojnica) ne smeta starim klijentima; preimenovano polje ih ruši.
  3. Jedinice i formati su dio ugovora: centi, a ne eura; kilometri u zahtjevu, metri u servisu mapa. Greška I-01 je bila greška u ugovoru o jedinicama.
Za radoznale

HTTP API se može testirati i ručno, alatima HTTPie (http POST :8000/v1/cijena … u terminalu) ili Postman (grafički program). Oni su odlični za istraživanje i za izvještaje o defektu („pošaljite ovaj zahtjev i vidjećete 500”). Za regresiju, koja se ponavlja pri svakoj izmjeni, bolji su automatski testovi kao test_api.py.

Ukratko – 9.9 Ugovor API-ja

  • Ugovor API-ja kaže koje putanje postoje, šta se šalje i šta se vraća, i na njega se oslanjaju svi klijenti.
  • FastAPI ugovor sam zapisuje u formatu OpenAPI, a test može provjeriti da su važne putanje tu.
  • Verzija u putanji omogućava velike promjene bez rušenja starih klijenata.
  • Polja se smiju dodavati, ali ne preimenovati ni brisati, a jedinice su dio ugovora.

Provjeri se

  1. Koji statusni kod vraća DeliveryApp kad se otkazuje narudžba u pripremi? 409 Conflict.
  2. Zašto klijent ne šalje cijenu jela serveru? Server ne vjeruje brojevima od klijenta; cijenu računa sam iz jelovnika.
  3. Šta je opasnije za postojeće klijente: dodati polje napojnica ili preimenovati ukupno u total? Preimenovati: stari klijenti traže ukupno i ne nalaze ga.

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

Praksa · Sedmica 9

Praktična nastava – Model, integracija i API DeliveryApp

Praktična nastava – Model, integracija i API DeliveryApp

Cilj je pokrenuti i proširiti testove modela, integracije i API-ja, naći grešku na šavu i provjeriti ugovor. Radi se u parovima. Prije početka: pip install fastapi httpx2 uvicorn.

Korak 1 – Model (40 minuta)

  1. Pokrenite pytest -q tests/test_model.py nad ispravnom i nad prvom verzijom. Zapišite koliko nizova pada.
  2. Pokrenite samo jedan pali niz: pytest -q "tests/test_model.py::test_slucajni_niz_radnji[6]". Dodajte print u test (ili pročitajte poruku) i zapišite cijeli niz radnji koji dovodi do pada. Koji je najkraći niz koji pokazuje D-04?
  3. Proširite model stanjem REKLAMACIJA iz vježbe 4 poglavlja 8 (samo u modelu). Šta se desi s testom nad sadašnjom klasom Narudzba? Da li je to greška u kodu ili u modelu?

Korak 2 – Integracija i dvojnici (50 minuta)

  1. Pokrenite pytest -q tests/test_narucivanje.py nad obje verzije. Za svaki pad na v0 zapišite poruku. Nađite red s greškom I-01 u narucivanje_v0.py.
  2. Napišite svoj stub StubMapeKm koji vraća 4.2 (kilometre) i test s njim nad v0. Prolazi li? Objasnite zašto je takav stub opasan.
  3. Dodajte test: kod STUDENT10 i adresa „Hrast 3” s 4 × Ćevapi. Očekivani iznos izračunajte iz pravila (vidi 8.4, kolona P4) i provjerite da je banka pozvana s tim iznosom.
  4. Dodajte test: iznos veći od 200,00 € (npr. 30 × Capricciosa na „Lipa 12”). Šta vraća PlacanjeServis? Šta mora uraditi naruci?

Korak 3 – API (60 minuta)

  1. Pokrenite pytest -q tests/test_api.py nad obje verzije i popunite tabelu: test, statusni kod, očekivano, dobijeno, greška (D-0x).
  2. Dopišite testove: GET /v1/jela?restoran=R-3 (tri jela, koja?); N-1002 preko POST /v1/cijena (32,00 €, dostava 0); kod " STUDENT10 " s razmacima; prelaz NOVA → ISPORUČENA (409).
  3. Pokrenite server (uvicorn api:app --reload) i isti zahtjev za N-1001 pošaljite ručno kroz stranicu /docs (ili HTTPie). Uporedite odgovor s testom.
  4. Pošaljite zahtjev bez polja kupac. Koji statusni kod dobijate i ko ga je vratio, naš kod ili FastAPI?

Korak 4 – Ugovor (30 minuta)

  1. U svojoj kopiji api.py preimenujte polje ukupno u total. Koji testovi padaju? Koji klijenti bi u stvarnosti „pali”?
  2. Vratite ime, pa dodajte novo polje "napojnica": 0 u odgovor. Pada li ijedan test? Zašto je to dozvoljena promjena ugovora, a preimenovanje nije?

Rezultat koji se predaje: fajl rezultati-9.md s brojem palih nizova modela i najkraćim nizom za D-04; opis greške I-01 s redom koda i objašnjenjem zašto je stub s kilometrima opasan; dopunjeni test_narucivanje.py i test_api.py; tabela API testova nad obje verzije; zaključak o promjenama ugovora iz koraka 4.

Vježbe

Vježbe za studenta

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

Vježba za studenta 1 – model.

Zadatak

Napišite model (rječnik) za jednostavan semafor na pješačkom prelazu: stanja CRVENO, ZELENO, TREPĆE_ZELENO; radnje „dugme” (iz CRVENO u ZELENO), „vrijeme” (iz ZELENO u TREPĆE_ZELENO, iz TREPĆE_ZELENO u CRVENO). Koliko je nedozvoljenih parova (stanje, radnja)?

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

Rješenje (R)
MODEL = {("CRVENO", "dugme"): "ZELENO",
         ("ZELENO", "vrijeme"): "TREPĆE_ZELENO",
         ("TREPĆE_ZELENO", "vrijeme"): "CRVENO"}

Parova (stanje, radnja) je 3 × 2 = 6, dozvoljena su 3, pa su 3 nedozvoljena: dugme u ZELENO, dugme u TREPĆE_ZELENO i vrijeme u CRVENO (može se i dogovoriti da se ti zahtjevi samo ignorišu, ali to mora pisati u specifikaciji).

Vježba za studenta 2 – vrsta dvojnika.

Zadatak

Koja je vrsta dvojnika: a) objekat koji uvijek vrati kurs 1,00 za bilo koju valutu; b) baza podataka u memoriji (SQLite) umjesto prave baze na serveru; c) objekat koji pamti sve poslane SMS poruke da bi ih test pročitao; d) objekat None poslan kao „logger” funkciji koja ga u tom testu ne koristi.

Vježba za studenta 2 – vrsta dvojnika. – rješenje

Rješenje (R)

a) stub; b) fake; c) špijun; d) dummy.

Vježba za studenta 3 – strategija.

Zadatak

Tim pravi novi modul „Dostavljači” koji zavisi od servisa za GPS (još nije gotov) i od klase Narudzba (gotova i testirana). Koju strategiju integracije i koje dvojnike predlažete?

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

Rješenje (R)

Odozgo nadolje za GPS: modul „Dostavljači” testirati sa stubom GPS servisa (zadane koordinate), napravljenim po ugovoru GPS tima. Za Narudzba koristiti pravu klasu, jer je gotova i testirana (odozdo nagore je već urađeno). Kad GPS servis bude gotov: ugovorni testovi za GPS i bar jedan test od kraja do kraja s pravim servisom.

Vježba za studenta 4 – statusni kodovi.

Zadatak

Koji statusni kod treba vratiti API DeliveryApp za: a) GET /v1/narudzbe/N-1001 kad narudžba postoji; b) POST /v1/narudzbe s ispravnom korpom; c) POST /v1/cijena sa iznosom 4,50 €; d) PATCH stanja ISPORUČENA → NOVA; e) GET /v1/narudzbe/N-0000.

Vježba za studenta 4 – statusni kodovi. – rješenje

Rješenje (R)

a) 200; b) 201; c) 422 (PP-1, poruka „iznos manji od minimalne narudžbe”); d) 409 (nedozvoljen prelaz); e) 404.

Vježba za studenta 5 – greška na šavu.

Zadatak

Servis plaćanja po ugovoru prima iznos u eurima kao decimalni broj (24.5), a Narucivanje šalje cente (2450). Šta se desi s N-1001? Koji test bi to uhvatio?

Vježba za studenta 5 – greška na šavu. – rješenje

Rješenje (R)

Banka bi naplatila 2450,00 € umjesto 24,50 €, sto puta više. Greška je na šavu: oba dijela su ispravna za sebe. Hvata je ugovorni test servisa plaćanja (šta prima i u kojoj jedinici) i integracioni test s vjernim fake servisom koji poštuje ugovor, uz provjeru špijunom da je naplaćeno tačno 24,50 €. (Usput: ugovor u eurima kao decimalnim brojevima je loš ugovor, poglavlje 5.)

Sažetak u pet rečenica

  1. Starliner je 2019. imao pogrešan tajmer misije jer je softver testiran u dijelovima, a cijeli let od lansiranja do spajanja nikad u jednom komadu.
  2. Testiranje zasnovano na modelu pravi testove iz malog, jasnog modela ponašanja, koji je i proročište: 200 slučajnih, ali ponovljivih nizova radnji poredi klasu Narudzba s modelom i nad prvom verzijom samo pronalazi D-04.
  3. Integraciono testiranje provjerava saradnju dijelova, strategijama veliki prasak, odozgo nadolje sa stubovima, odozdo nagore s drajverima ili sendvič, a uvijek završava testom od kraja do kraja.
  4. Test dvojnici (dummy, stub, špijun, mock, fake) glume dijelove koje ne želimo zvati, ali moraju poštovati ugovor, jer su greške na šavu, kao metri poslani kao kilometri u I-01, nevidljive jediničnim testovima.
  5. API DeliveryApp testira se kroz FastAPI TestClient provjerom statusnih kodova i sadržaja, a ugovor (putanje, polja, jedinice) štite ugovorni testovi i pravilo da se polja dodaju, ali ne preimenuju.

Pitanja za ponavljanje

  1. Zašto testovi na zemlji nisu našli grešku tajmera Starlinera?
  2. Šta je model u testiranju? Navedite tri vrste modela.
  3. Koje su dvije uloge modela u testiranju zasnovanom na modelu?
  4. Kako radi test test_slucajni_niz_radnji i šta je našao nad prvom verzijom?
  5. Zašto slučajni testovi moraju imati sjeme?
  6. Opišite četiri strategije integracije s prednostima i manama.
  7. Šta je stub, a šta drajver? Dajte primjer iz DeliveryApp.
  8. Nabrojte pet vrsta test dvojnika i objasnite razliku.
  9. Šta provjerava test test_izvan_zone_banka_se_ne_poziva i zašto je to važno?
  10. Šta je greška I-01 i zašto je jedinični testovi ne vide?
  11. Zašto stub mora biti napravljen iz ugovora, a ne iz onoga što misli kod?
  12. Šta su statusni kodovi 200, 201, 404, 409 i 422 i kada ih vraća DeliveryApp?
  13. Kako TestClient testira API bez servera? Napišite test za N-1001.
  14. Šta je ugovor API-ja i koje promjene ugovora su sigurne?