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
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. | Poglavlje | Teorija (3 časa) | Praksa (3 časa) |
|---|---|---|---|
| 1 | Zašto testiramo | Uvodne priče; greška, defekt, otkaz; sedam principa; DeliveryApp | Tabela očekivanih cijena rukom |
| 2 | Proces i nivoi | Test proces; nivoi i V-model; vrste; statičko testiranje | Recenzija zahtjeva DeliveryApp |
| 3 | Manuelno testiranje | Test slučaj; izvještaj o defektu; istraživačko testiranje | Test slučajevi za web stranicu |
| 4 | Automatizovano testiranje | Šta automatizovati; piramida testova; prvi test | Prvi pytest |
| 5 | Jedinično testiranje | AAA; fixture, parametri, izuzeci; TDD | Testovi za dostavu i popust |
| 6 | Bijela kutija | Graf toka; pokrivenost; ciklomatska složenost | coverage.py do 100 % grana |
| 7 | Tok podataka | Definicija i upotreba; anomalije; statička analiza, mutacije | ruff, mypy, ručna mutacija |
| 8 | Crna kutija | Ekvivalencija i granice; tabela odlučivanja; stanja | Lov na pet grešaka |
| Sed. | Poglavlje | Teorija (3 časa) | Praksa (3 časa) |
|---|---|---|---|
| 9 | Model i integracija | Model; strategije integracije; API | TestClient za API |
| 10 | Sistemsko testiranje | Sistemsko; performanse i sigurnost; prihvatno i BDD | Locust, Gherkin |
| 11 | OO testiranje | Stanje objekta; nasljeđivanje; dvojnici | Narudzba, unittest.mock |
| 12 | Paralelne aplikacije | Trka podataka; zastoj; nestabilni testovi | Niti, 1000 ponavljanja |
| 13 | Web aplikacije | Web i DOM; Playwright; Page Object, pristupačnost | Playwright za DeliveryApp |
| 14 | Upravljanje testiranjem | Test plan i rizik; metrike; CI/CD | GitHub Actions |
| 15 | Alati, AI, projekat | Pregled alata; AI u testiranju; ISTQB | Odbrana projekta |
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.

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.
POST /v1/cijena.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.
| Čas | Sadržaj | Na času |
|---|---|---|
| Teorija 1 | 9.1–9.3 Model kao izvor testova; slučajni nizovi radnji | 200 nizova radnji za jednu narudžbu |
| Teorija 2 | 9.4–9.6 Strategije integracije; test dvojnici; greška na šavu | Metri ili kilometri? |
| Teorija 3 | 9.7–9.9 API, HTTP i statusni kodovi; TestClient; ugovor | POST /v1/cijena za N-1001 |
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:
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:
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.
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:
@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}"Šta se dešava u svakom koraku:
Narudzba mora javiti ValueError;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.13sNad 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.18sJedanaest 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.
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:
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).
Odgovori: tipka R ili klik na zamućeno polje.
DeliveryApp nije jedna funkcija. Kad kupac naruči, dešava se lanac:
ukupno, koja zove cijena_dostave i popust);Taj lanac je u fajlu primjeri/deliveryapp/narucivanje.py:
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, transakcijaKojim redom spajati i testirati dijelove? Postoje četiri strategije:
| Strategija | Kako | Prednost | Mana |
|---|---|---|---|
| veliki prasak (big bang) | sve se spoji odjednom i testira | nema dodatnog koda | kad padne, ne zna se gdje; greške se nađu kasno |
| odozgo nadolje (top-down) | prvo gornji dio (Narucivanje), a donji se zamijene stubovima | rano se vidi cijeli tok | treba pisati stubove |
| odozdo nagore (bottom-up) | prvo donji dijelovi (cijena_dostave, popust), a gornji glumi drajver | donji dijelovi su temeljno testirani | cijeli tok se vidi tek na kraju |
| sendvič | odozgo i odozdo u isto vrijeme, sastaju se u sredini | brzo, više timova radi paralelno | složenije za organizovati |

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

| Vrsta | Šta radi | U DeliveryApp |
|---|---|---|
| dummy | samo popunjava parametar; nikad se ne koristi | objekat poslan funkciji koja ga ne dira |
| stub | vraća unaprijed zadan odgovor | StubMape(4200): svaka adresa je 4200 m |
| špijun (spy) | radi kao stub, ali zapamti kako je pozvan | SpyPlacanje: pamti iznose u listi pozivi |
| mock | unaprijed zna koje pozive mora dobiti i sam provjeri | poglavlje 11 (unittest.mock) |
| fake | zaista radi, ali pojednostavljeno | PlacanjeServis: lista u memoriji umjesto prave banke |
Dvojnici u tests/test_narucivanje.py su obične male klase, bez ikakve biblioteke:
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"I testovi, svaki s očekivanim rezultatom iz pravila:
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 dostavljenaDrugi 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.
„Š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.
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.
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.16sSvaki 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.
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.
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.
Odgovori: tipka R ili klik na zamućeno polje.
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:
| Metoda i putanja | Šta radi | Uspješan odgovor |
|---|---|---|
GET /v1/jela | jelovnik (opciono ?restoran=R-2) | 200 i lista jela |
POST /v1/cijena | izračuna cijenu bez pravljenja narudžbe | 200 i račun |
POST /v1/narudzbe | napravi narudžbu | 201 i broj narudžbe |
GET /v1/narudzbe/{broj} | stanje i istorija narudžbe | 200 |
PATCH /v1/narudzbe/{broj}/stanje | promijeni stanje | 200; 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}]}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:
| Kod | Značenje | Kada u DeliveryApp |
|---|---|---|
| 200 OK | uspjeh | cijena izračunata, stanje promijenjeno |
| 201 Created | nešto je napravljeno | nova narudžba |
| 404 Not Found | ne postoji | jelo 99, narudžba N-9999 |
| 409 Conflict | sukob sa stanjem | otkazivanje narudžbe u pripremi (PP-6) |
| 422 Unprocessable | podaci nisu ispravni | 11 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.
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):
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 kodaZapis {**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:
@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"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.40sIste 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.
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?
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 putanjeTri pravila za ugovore, koja i DeliveryApp poštuje:
/v1/). Kad treba velika promjena, napravi se /v2/, a /v1/ radi dok svi klijenti ne pređu.napojnica) ne smeta starim klijentima; preimenovano polje ih ruši.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.
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.
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.
pytest -q tests/test_model.py nad ispravnom i nad prvom verzijom. Zapišite koliko nizova pada.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?Narudzba? Da li je to greška u kodu ili u modelu?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.StubMapeKm koji vraća 4.2 (kilometre) i test s njim nad v0. Prolazi li? Objasnite zašto je takav stub opasan.PlacanjeServis? Šta mora uraditi naruci?pytest -q tests/test_api.py nad obje verzije i popunite tabelu: test, statusni kod, očekivano, dobijeno, greška (D-0x).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).uvicorn api:app --reload) i isti zahtjev za N-1001 pošaljite ručno kroz stranicu /docs (ili HTTPie). Uporedite odgovor s testom.kupac. Koji statusni kod dobijate i ko ga je vratio, naš kod ili FastAPI?api.py preimenujte polje ukupno u total. Koji testovi padaju? Koji klijenti bi u stvarnosti „pali”?"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.
Rješenja su sakrivena – tipka R ili klik na okvir.
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)?
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).
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.
a) stub; b) fake; c) špijun; d) dummy.
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?
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.
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.
a) 200; b) 201; c) 422 (PP-1, poruka „iznos manji od minimalne narudžbe”); d) 409 (nedozvoljen prelaz); e) 404.
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?
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.)
Narudzba s modelom i nad prvom verzijom samo pronalazi D-04.test_slucajni_niz_radnji i šta je našao nad prvom verzijom?test_izvan_zone_banka_se_ne_poziva i zašto je to važno?