Dio 1 – Klasa i stanje
Dio 2 – Nasljeđivanje i polimorfizam
Dio 3 – Mock i patch
Praksa – Objekti, podklase i mockovi
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 11 (Sedmica 11, 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 |
Sredinom osamdesetih godina u bolnicama u SAD-u i Kanadi radio je Therac-25, aparat za liječenje raka zračenjem, koji je pravila kanadska firma AECL. Aparat je imao dva načina rada: slabi snop elektrona i vrlo jak snop koji se, prije nego što stigne do pacijenta, mora rasuti kroz metalnu ploču. Ako jak snop krene bez ploče, pacijent dobije dozu stotinama puta veću od propisane. Između juna 1985. i januara 1987. desilo se šest poznatih nesreća s takvim prevelikim dozama; neki pacijenti su umrli, drugi su ostali teško povrijeđeni. Aparat je pri tome pokazivao kratku poruku „MALFUNCTION 54”, koju su operateri navikli preskakati, jer se javljala često i bez posljedica. Uzrok je otkriven tek kada je fizičar bolnice u Teksasu sam sjeo za aparat i naučio da kuca tačno onako brzo kao iskusna operaterka. Ako je operaterka ispravila jedno polje u manje od oko osam sekundi, softver nije primijetio izmjenu: stanje u memoriji i stanje mašine se više nisu slagali.
Kasnija analiza, koju su objavili Nancy Leveson i Clark Turner 1993., našla je još dublji uzrok. Velik dio softvera preuzet je sa starijih modela Therac-6 i Therac-20. Na starijim modelima iste greške u softveru postojale su, ali su ih hardverske brave (mehanički osigurači) zaustavljale prije nego što bi naudile pacijentu. Therac-25 je te brave izbacio, jer se vjerovalo softveru „koji godinama radi bez problema”. Stari kod je u novoj mašini izgubio zaštitu na koju je, ne znajući, računao. Ovo poglavlje govori o tome šta to znači za testiranje programa sastavljenih od objekata koji imaju stanje, koji se nasljeđuju i ponovo koriste u novim okolnostima.

DeliveryApp je napisan objektno: klasa Narudzba čuva stanje i istoriju, a Narucivanje sarađuje sa servisima mapa i plaćanja. U poglavljima 8 i 9 testirali smo prelaze stanja i napravili ručne test dvojnike. Sada gledamo šta je posebno u testiranju objekata. Prvo klasu i stanje: zašto se klasa testira nizovima metoda i šta je invarijanta. Zatim nasljeđivanje i polimorfizam: zašto naslijeđena metoda koja radi u roditelju može pasti u podklasi, i kako isti testovi provjeravaju sve podklase. Na kraju alat unittest.mock, koji pravi test dvojnike automatski, i tehniku patch, kojom se u testu zamjenjuje i ono što je teško zamijeniti, kao trenutno vrijeme.
Narudzba), objekat je jedan primjerak s vlastitim podacima (narudžba N-1001).n.promijeni(...)) radi različito zavisno od vrste objekta.create_autospec – pravi mock koji ima samo metode i parametre prave klase.patch – privremeno zamijeni ime u nekom modulu dvojnikom, samo za vrijeme testa.Nakon poglavlja možete:
objasniti po čemu se testiranje objekata razlikuje od testiranja funkcija; testirati klasu kroz nizove metoda i stanje; napisati i provjeravati invarijantu klase; objasniti zašto naslijeđena metoda mora biti ponovo testirana u podklasi; primijeniti Liskovin princip zamjene; napisati testove roditelja koji se automatski izvršavaju nad svim podklasama; napraviti mock sa unittest.mock, zadati mu povratnu vrijednost ili izuzetak i provjeriti pozive; objasniti zašto je create_autospec sigurniji od običnog mocka; zamijeniti trenutno vrijeme u testu tehnikom patch; prepoznati pretjerano korištenje mockova.
| Čas | Sadržaj | Na času |
|---|---|---|
| Teorija 1 | 11.1–11.3 Šta je posebno u objektima; stanje i nizovi metoda; invarijanta | Istorija mora završavati trenutnim stanjem |
| Teorija 2 | 11.4–11.6 Nasljeđivanje; Liskovin princip; ugovorni testovi za podklase; polimorfizam | Narudžba za preuzimanje i KeyError |
| Teorija 3 | 11.7–11.9 unittest.mock, create_autospec, patch; kad mockova ima previše | Banka ne odgovara |
Funkcija cijena_dostave je kao kalkulator: isti ulaz, isti izlaz, uvijek. Objekat je više kao automat za kafu: ono što se desi kad pritisnete dugme zavisi od toga šta se desilo prije (da li ste ubacili novac, da li je ostalo mlijeka). Zato testiranje objekata ima četiri posebna izazova:
| Osobina | Izazov za testera | U DeliveryApp |
|---|---|---|
| stanje | rezultat metode zavisi od prethodnih poziva; testiraju se nizovi metoda | promijeni("OTKAZANA") radi u NOVA, a ne radi u U_PRIPREMI |
| enkapsulacija | podaci su skriveni; test ih vidi samo kroz javne metode i atribute | stanje se mijenja samo kroz promijeni, ne direktno |
| nasljeđivanje | podklasa preuzima metode koje su testirane u drugom kontekstu | NarudzbaZaPreuzimanje (11.4) |
| polimorfizam | isti poziv radi različito za različite klase; svaka varijanta treba test | n.promijeni(...) za dvije vrste narudžbe |
Dobra vijest: alati koje već znamo pokrivaju veliki dio. Prelazi stanja (poglavlje 8) i model (poglavlje 9) su upravo testovi nizova metoda. Ovdje dodajemo invarijante, nasljeđivanje i automatske dvojnike.
Neke stvari o narudžbi moraju biti tačne uvijek, bez obzira kroz koje je metode prošla:
Takva tvrdnja zove se invarijanta klase (od latinskog „nepromjenljiv”). Slika iz života: knjigovodstvo. Šta god se knjižilo tokom dana, na kraju aktiva mora biti jednaka pasivi. Ako nije, negdje je greška, i to se vidi odmah, bez obzira koja knjiženja su urađena.
Invarijanta se u testu piše jednom, kao pomoćna funkcija, i provjerava poslije svake metode:
def provjeri_invarijantu(n):
assert n.istorija[0] == "NOVA"
assert n.istorija[-1] == n.stanje
assert all(a != b for a, b in zip(n.istorija, n.istorija[1:])) # nema „prelaza” u isto stanje
def test_invarijanta_kroz_cijeli_put():
n = Narudzba("N-1001")
provjeri_invarijantu(n)
for s in ["PRIHVAĆENA", "U_PRIPREMI", "NA_PUTU", "ISPORUČENA"]:
n.promijeni(s)
provjeri_invarijantu(n)Posebno je važno provjeriti invarijantu poslije neuspjele metode. Metoda koja javi grešku ne smije ostaviti objekat „napola promijenjen”:
def test_invarijanta_poslije_odbijenog_prelaza():
n = Narudzba("N-1001")
with pytest.raises(ValueError):
n.promijeni("ISPORUČENA")
provjeri_invarijantu(n) # neuspjela metoda ne smije pokvariti objekat
assert n.istorija == ["NOVA"]Zamislite da je programer u promijeni napisao prvo self.istorija.append(novo), pa tek onda provjeru. Prelaz bi bio odbijen, ali bi „ISPORUČENA” ostala zapisana u istoriji. Stanje bi bilo NOVA, a istorija bi govorila drugo: stanje u objektu i zapis o njemu se više ne bi slagali, upravo kao memorija i mašina kod Therac-25. Invarijanta bi to uhvatila odmah.
Invarijanta se može dodati i u model iz poglavlja 9: poslije svakog od 1600 koraka slučajnih nizova pozovite provjeri_invarijantu(n). Tako jedan red koda provjerava stanje objekta u hiljadama situacija.
Objekat skriva svoje unutrašnje podatke (enkapsulacija). Da li test smije „zaviriti” unutra? Pravilo je: testirajte kroz javni interfejs, kao što objekat koriste drugi dijelovi programa. Za Narudzba to su metode promijeni i moze_otkazati i atributi stanje i istorija.
Zašto ne direktno? Uporedite dva testa:
# Loše: test postavlja stanje „na silu”, mimo pravila
n = Narudzba("N-1001")
n.stanje = "U_PRIPREMI" # istorija je i dalje ["NOVA"]: objekat je u nemogućem stanju
# Dobro: test dovodi objekat u stanje istim putem kao pravi program
n = Narudzba("N-1001")
n.promijeni("PRIHVAĆENA")
n.promijeni("U_PRIPREMI")Prvi test pravi objekat koji u stvarnom programu ne može postojati (stanje U_PRIPREMI s istorijom samo NOVA). Šta god da provjeri, ne govori ništa o stvarnom ponašanju. Zato fixture prihvacena iz poglavlja 5 i funkcija narudzba_u_stanju iz poglavlja 8 dovode narudžbu u stanje kroz dozvoljene prelaze.
Drugi razlog: test koji zavisi od unutrašnjosti puca pri svakom refaktorisanju. Ako programer sutra istoriju čuva kao listu parova (stanje, vrijeme), testovi kroz javni interfejs i dalje rade, a testovi koji čitaju unutrašnje podatke moraju se mijenjati, iako se ponašanje nije promijenilo.
n.stanje = "U_PRIPREMI"? Pravi objekat koji u programu ne može postojati (istorija se ne slaže sa stanjem), pa test ne govori ništa o stvarnom ponašanju.Odgovori: tipka R ili klik na zamućeno polje.
DeliveryApp uvodi novu uslugu: kupac može hranu preuzeti sam u restoranu. Takva narudžba nema dostave, pa umjesto NA_PUTU i ISPORUČENA ima stanja SPREMNA i PREUZETA. Programer pravi podklasu koja nasljeđuje Narudzba i mijenja samo metodu promijeni, s novom tabelom prelaza:
PRELAZI_PREUZIMANJE = {
"NOVA": {"PRIHVAĆENA", "ODBIJENA", "OTKAZANA"},
"PRIHVAĆENA": {"U_PRIPREMI", "OTKAZANA"},
"U_PRIPREMI": {"SPREMNA"},
"SPREMNA": {"PREUZETA"},
"PREUZETA": set(),
"ODBIJENA": set(),
"OTKAZANA": set(),
}
class NarudzbaZaPreuzimanje(Narudzba):
"""Narudžba koju kupac sam preuzima u restoranu."""
def promijeni(self, novo):
if novo not in PRELAZI_PREUZIMANJE[self.stanje]:
raise ValueError(f"nedozvoljen prelaz {self.stanje} -> {novo}")
self.stanje = novo
self.istorija.append(novo)Metoda moze_otkazati je naslijeđena od roditelja, nepromijenjena. Ona je testirana, radi godinama, šta bi moglo poći naopako? Pogledajmo je:
def moze_otkazati(self):
return "OTKAZANA" in PRELAZI[self.stanje] # PRELAZI roditelja!Ona gleda tabelu roditelja, u kojoj nema stanja SPREMNA. Dok je narudžba za preuzimanje u stanjima koja postoje u obje tabele (NOVA, PRIHVAĆENA, U_PRIPREMI), sve radi. Kad dođe u stanje SPREMNA:
>>> n.moze_otkazati()
return "OTKAZANA" in PRELAZI[self.stanje]
~~~~~~~^^^^^^^^^^^^^
KeyError: 'SPREMNA'
Ovo je Therac-25 u malom: kod koji je ispravan u starom kontekstu pada u novom, jer je računao na nešto što je u novom kontekstu promijenjeno. Pouka za testera je jasna i ponekad iznenađuje: naslijeđena metoda mora se ponovo testirati u podklasi, iako u njoj nije promijenjen nijedan red. Promijenio se kontekst: podaci, stanja i metode s kojima ona sarađuje.
Ispravka u primjeri/deliveryapp/vrste_narudzbi.py: podklasa zamjenjuje i moze_otkazati, tako da gleda njenu tabelu. (Još bolji dizajn bio bi da roditelj tabelu čuva kao atribut klase, self.PRELAZI, pa da podklasa zamijeni samo tabelu; to je zadatak u praktičnoj nastavi.)
Programeri ovaj problem zovu „krhka bazna klasa” (fragile base class): podklasa zavisi od detalja roditelja, pa i mala promjena na jednoj strani ruši drugu. Zato mnogi savjetuju „radije sastavljaj nego nasljeđuj” (composition over inheritance): umjesto podklase, objekat koji koristi drugi objekat, npr. narudžba koja dobije tabelu prelaza kao parametar.
moze_otkazati naslijeđena u narudžbi za preuzimanje pada s KeyError u stanju SPREMNA.Kad je podklasa „dobra”? Odgovor je dala Barbara Liskov, profesorica s MIT-a, i zove se Liskovin princip zamjene: objekat podklase mora moći zamijeniti objekat roditelja na svakom mjestu u programu, a da program i dalje radi ispravno. Dio programa koji radi s narudžbom (npr. dugme „Otkaži” na ekranu) ne smije morati znati da li je narudžba za dostavu ili za preuzimanje.
Za narudžbu to znači da obje vrste moraju poštovati ugovor roditelja:
moze_otkazati() se u svakom stanju slaže s tim da li promijeni("OTKAZANA") uspijeva, i nikad ne javlja grešku.Kako testirati ugovor za sve podklase, a ne pisati iste testove dva puta? pytest ima odgovor: parametrizovan fixture. Fixture nova vraća jednom Narudzba, a drugi put NarudzbaZaPreuzimanje, i svaki test koji ga koristi izvrši se za obje:
@pytest.fixture(params=[Narudzba, NarudzbaZaPreuzimanje], ids=["dostava", "preuzimanje"])
def nova(request):
"""Svaki test ispod izvrši se jednom za svaku vrstu narudžbe."""
return request.param("N-1001")
def test_ugovor_u_pripremi_nema_otkazivanja(nova):
nova.promijeni("PRIHVAĆENA")
nova.promijeni("U_PRIPREMI")
assert not nova.moze_otkazati()
with pytest.raises(ValueError):
nova.promijeni("OTKAZANA") # PP-6 važi za obje vrste
def test_ugovor_moze_otkazati_u_svakom_stanju(nova):
"""moze_otkazati() mora se slagati s promijeni() u svakom stanju na putu narudžbe."""
put = ["PRIHVAĆENA", "U_PRIPREMI", "NA_PUTU", "ISPORUČENA"] if type(nova) is Narudzba \
else ["PRIHVAĆENA", "U_PRIPREMI", "SPREMNA", "PREUZETA"]
for s in put:
nova.promijeni(s)
nova.moze_otkazati() # ne smije javiti grešku ni u jednom stanjupytest -v tests/test_oo.py -k ugovor
tests/test_oo.py::test_ugovor_pocinje_kao_nova[dostava] PASSED
tests/test_oo.py::test_ugovor_pocinje_kao_nova[preuzimanje] PASSED
...
tests/test_oo.py::test_ugovor_moze_otkazati_u_svakom_stanju[dostava] PASSED
tests/test_oo.py::test_ugovor_moze_otkazati_u_svakom_stanju[preuzimanje] PASSEDPosljednji test je onaj koji hvata grešku iz 11.4: s naslijeđenom moze_otkazati varijanta [preuzimanje] pada s KeyError: 'SPREMNA'. Kad sutra neko doda treću vrstu (npr. NarudzbaZaFirmu), dovoljno je dodati je u listu params, i svi ugovorni testovi se sami izvrše i nad njom.
Nad prvom verzijom (DeliveryApp=v0) pada test_ugovor_u_pripremi_nema_otkazivanja[dostava]: naš stari poznanik D-04. Zanimljivo, varijanta [preuzimanje] prolazi, jer podklasa ima svoju tabelu prelaza u kojoj greške nema.
params i odmah prolazi kroz sve ugovorne testove.Polimorfizam znači da isti poziv radi različito zavisno od vrste objekta. Dugme „Sljedeći korak” u aplikaciji restorana poziva n.promijeni(...), a da li je sljedeće stanje NA_PUTU ili SPREMNA, zna sam objekat. To je velika prednost za programera, ali obaveza za testera: svaka varijanta mora biti testirana na mjestu gdje se koristi.
Praktična pravila:
test_preuzimanje_nema_dostave: narudžba za preuzimanje ne smije ići u NA_PUTU).Broj kombinacija raste brzo: tri vrste narudžbe × četiri mjesta poziva = 12 kombinacija. Ovdje pomaže testiranje po parovima iz poglavlja 8, i to što ugovorni testovi već pokrivaju zajedničko ponašanje.
„Podklasa ima samo jednu novu metodu, pa testiram samo tu metodu.” Polimorfizam radi i u suprotnom smjeru: naslijeđene metode roditelja mogu pozivati zamijenjenu metodu podklase, pa nova metoda mijenja ponašanje starih. Zato se pri dodavanju podklase pokreću svi testovi roditelja nad njom.
moze_otkazati sa SPREMNA.params.Odgovori: tipka R ili klik na zamućeno polje.
U poglavlju 9 napisali smo test dvojnike ručno: StubMape, SpyPlacanje. Za veće projekte to je mnogo pisanja. Python ima ugrađeni modul unittest.mock koji pravi dvojnike automatski. Najvažnije mogućnosti:
| Šta | Kako | Primjer |
|---|---|---|
| napraviti dvojnika | Mock() ili create_autospec(Klasa, instance=True) | placanje = create_autospec(PlacanjeServis, instance=True) |
| zadati povratnu vrijednost | .return_value | placanje.naplati.return_value = "T-0042" |
| zadati izuzetak | .side_effect | placanje.naplati.side_effect = ConnectionError(...) |
| provjeriti poziv | .assert_called_once_with(...) | placanje.naplati.assert_called_once_with(2450) |
| provjeriti da nije pozvan | .assert_not_called() | banka se ne zove za narudžbu izvan zone |
| pročitati sve pozive | .call_args_list, .call_count | koliko puta je banka pozvana |
Test iz tests/test_oo.py:
def test_mock_banka_pozvana_jednom_s_iznosom():
placanje = create_autospec(PlacanjeServis, instance=True)
placanje.naplati.return_value = "T-0042"
mape = Mock()
mape.udaljenost_m.return_value = 4200
_, iznos, transakcija = Narucivanje(mape, placanje).naruci(N1001, "Lipa 12")
assert (iznos, transakcija) == (2450, "T-0042")
placanje.naplati.assert_called_once_with(2450)
mape.udaljenost_m.assert_called_once_with("Lipa 12")Ovo je mock u pravom smislu riječi (poglavlje 9, slika 9.3): test unaprijed zna kako dvojnik mora biti pozvan („tačno jednom, sa 2450”) i to provjeri. Ako bi naruci greškom pozvao banku dva puta (kupac plati dvaput!), assert_called_once_with bi pao s porukom koliko je poziva bilo.
Još korisnije je glumiti kvar, koji je s pravim servisom teško izazvati:
def test_mock_banka_nedostupna():
placanje = create_autospec(PlacanjeServis, instance=True)
placanje.naplati.side_effect = ConnectionError("banka ne odgovara")
mape = Mock(**{"udaljenost_m.return_value": 4200})
narucivanje = Narucivanje(mape, placanje)
with pytest.raises(ConnectionError):
narucivanje.naruci(N1001, "Lipa 12")
assert narucivanje.brojac == 1000 # narudžba nije napravljenaBanka „ne odgovara”, a test provjerava da narudžba nije napravljena: kupac ne smije dobiti potvrdu za neplaćenu narudžbu.
unittest.mock automatski pravi test dvojnike.return_value zadaje odgovor, a side_effect izuzetak ili niz odgovora.assert_called_once_with provjerava da je dvojnik pozvan tačno jednom s datim argumentima.Obični Mock() ima opasnu osobinu: prihvata sve. Ako u kodu stoji pogrešno ime metode, npr. placanje.naplatiti(iznos) umjesto naplati, obični mock će vratiti novi mock i ništa neće prijaviti. Test prolazi, a s pravom bankom program pada. Zato se za klase koje imamo koristi create_autospec: dvojnik dobija samo metode i parametre koje prava klasa ima.
def test_autospec_hvata_pogresno_ime_metode():
placanje = create_autospec(PlacanjeServis, instance=True)
with pytest.raises(AttributeError):
placanje.naplatiti(2450) # takve metode nema u pravoj klasiTo je ista pouka kao u poglavlju 9: dvojnik mora poštovati ugovor pravog dijela. create_autospec taj ugovor uzima automatski iz prave klase.
Drugi alat je patch: privremeno zamijeni neko ime u modulu, samo dok traje test. Klasičan primjer je vrijeme. Fajl primjeri/deliveryapp/radno_vrijeme.py:
RADNO_VRIJEME = {"R-1": (10, 23), "R-2": (9, 22), "R-3": (8, 20)} # od, do (puni sati)
def sada():
"""Trenutno vrijeme. Izdvojeno u funkciju da bi ga test mogao zamijeniti."""
return dt.datetime.now()
def otvoren(restoran):
"""Da li restoran sada prima narudžbe."""
od, do = RADNO_VRIJEME[restoran]
return od <= sada().hour < doKako testirati da je Picerija zatvorena u 9 h, ako test pokrećemo u podne? Ne čekamo devet sati sutra ujutro, nego zamijenimo funkciju sada:
@pytest.mark.parametrize("sat, ocekivano", [(9, False), (10, True), (22, True), (23, False)])
def test_picerija_radno_vrijeme(sat, ocekivano):
with patch("radno_vrijeme.sada", return_value=dt.datetime(2026, 10, 22, sat, 0)):
assert otvoren("R-1") is ocekivano # R-1 radi od 10 do 23 hPrimijetite dvije stvari. Prvo, ovo su granične vrijednosti iz poglavlja 8: 9 i 10 h na otvaranju, 22 i 23 h na zatvaranju. Drugo, programer je trenutno vrijeme izdvojio u funkciju sada upravo zato da bi se moglo zamijeniti. To je testabilan dizajn iz poglavlja 5: kod koji direktno zove sat, slučajni broj ili mrežu teško je testirati, a mali „šav” (seam) to rješava.
Gdje u kodu još postoji „skrivena zavisnost” koju bi trebalo izdvojiti da bi se mogla zamijeniti u testu? – Npr. generator broja narudžbe (N-1001, N-1002…), slučajni izbor dostavljača, slanje SMS-a kupcu, čitanje kursa eura iz banke.
Mock() prihvata svaku metodu, pa ne hvata pogrešno ime metode u kodu.create_autospec pravi dvojnika samo s metodama prave klase i tako poštuje ugovor.patch privremeno zamijeni ime u modulu, npr. funkciju koja vraća trenutno vrijeme.Mockovi su moćni, i zato opasni. Tri znaka da ih je previše:
cijena_dostave pozvana tačno jednom, sa 4.2 i 2150, puca čim programer promijeni unutrašnjost (npr. izračuna dostavu ranije), iako je rezultat isti. Bolje: provjeriti rezultat (2450) i samo one pozive koji su dio ugovora sa spoljnim svijetom (naplata u banci, slanje SMS-a).ukupno, popust) su brza i bez spoljnih zavisnosti; njih ne treba mockovati, nego koristiti prave. Mock je za ono što je sporo, skupo, nepouzdano ili tuđe: banka, mapa, mreža, sat.Najveća opasnost je stara poznanica iz poglavlja 9: mock je pretpostavka. Svi testovi s mockovima mogu biti zeleni, a sistem s pravom bankom ne raditi. Zato piramida iz poglavlja 4 ima i integracione i sistemske testove iznad jediničnih.
Therac-25 pokazuje i ovu pouku. Softver je „testiran” na starijim mašinama, u okruženju koje se razlikovalo od novog upravo u onome što je bilo važno: hardverska brava je bila dvojnik koji je gutao greške. Kad se okruženje promijenilo, testovi koji su ranije „prolazili” nisu više značili ništa.
side_effect? Zada izuzetak (ili niz odgovora) koji mock javlja kad se pozove, npr. ConnectionError za banku koja ne odgovara.create_autospec sigurniji od Mock()? Jer ima samo metode i parametre prave klase, pa pogrešno ime metode odmah javi AttributeError.Odgovori: tipka R ili klik na zamućeno polje.
Cilj je provjeriti invarijante narudžbe, napraviti novu podklasu i provjeriti je ugovornim testovima, i zamijeniti spoljne zavisnosti mockovima i patch. Radi se u parovima.
pytest -v tests/test_oo.py nad ispravnom i nad prvom verzijom; zapišite rezultate.deliveryapp.py zamijenite redoslijed u promijeni: prvo self.istorija.append(novo), pa provjera. Koji test pada? Da li bi pao i neki test iz poglavlja 8?provjeri_invarijantu(n) poslije svakog koraka u testu modela iz poglavlja 9 (tests/test_model.py) i pokrenite ga nad obje verzije.vrste_narudzbi.py obrišite metodu moze_otkazati iz podklase. Pokrenite pytest -v tests/test_oo.py -k ugovor. Koji test pada i s kojom porukom?deliveryapp.py neka klasa Narudzba ima atribut klase PRELAZI, a metode koriste self.PRELAZI. Tada podklasa zamijeni samo tabelu, bez ijedne metode. Pokrenite sve testove.NarudzbaZaFirmu: firma plaća mjesečno, pa nakon ISPORUČENA ide stanje FAKTURISANA. Napišite tabelu prelaza iz specifikacije, prije koda.params fixturea nova i pokrenite ugovorne testove. Šta treba dopuniti u testu test_ugovor_moze_otkazati_u_svakom_stanju da bi radio za tri vrste? (Savjet: neka svaka klasa ima listu „puta” ili neka test čita put iz tabele.)KeyError (nepoznata adresa). Šta treba da se desi s narudžbom? Da li je banka pozvana? Provjerite sa assert_not_called().ConnectionError, a drugi put vrati transakciju (side_effect kao lista). naruci danas ne pokušava ponovo; zapišite kao pitanje za vlasnika proizvoda da li bi trebao.patch.cijena_dostave pozvana s 4.2 i 2150 (koristite patch("narucivanje.ukupno", wraps=...) ili sličan pristup). Zatim zamislite refaktorisanje koje ne mijenja rezultat, a obara taj test. Objasnite u dvije rečenice zašto je test loš.Rezultat koji se predaje: fajl rezultati-11.md s rezultatima nad obje verzije, opisom testa koji je uhvatio pogrešan redoslijed u promijeni i greške naslijeđene metode; izmijenjen deliveryapp.py sa self.PRELAZI; NarudzbaZaFirmu s tabelom prelaza i testovima; novi testovi s mockovima i patch; kratko objašnjenje lošeg testa iz koraka 4.4.
Rješenja su sakrivena – tipka R ili klik na okvir.
Klasa Korpa ima listu stavke (parovi cijena, količina) i atribut ukupno, koji se ažurira u metodama dodaj i ukloni. Napišite invarijantu.
ukupno je uvijek jednak zbiru cijena × količina svih stavki, i nijedna količina nije manja od 1 (stavka s količinom 0 se uklanja). U kodu:
def provjeri_invarijantu(k):
assert k.ukupno == sum(c * q for c, q in k.stavke)
assert all(q >= 1 for _, q in k.stavke)Provjerava se poslije svakog dodaj i ukloni, i poslije neuspjelog ukloni (uklanjanje stavke koje nema).
Programer pravi podklasu HitnaNarudzba u kojoj se narudžba može otkazati i u stanju U_PRIPREMI, „jer je hitna”. Da li to krši Liskovin princip? Šta bi pokazali ugovorni testovi?
Krši: dio programa koji radi s bilo kojom narudžbom oslanja se na PP-6 (u pripremi nema otkazivanja; restoran je već potrošio namirnice). HitnaNarudzba se ne ponaša kao roditelj, pa je ne može zamijeniti. Ugovorni test test_ugovor_u_pripremi_nema_otkazivanja bi za nju pao. Ako posao zaista traži drugačije pravilo, to nije podklasa iste narudžbe, nego posebna vrsta s posebnim pravilom koje se mora dogovoriti i zapisati.
Napišite test koji provjerava da se kupcu šalje SMS „Narudžba N-1001 je na putu” kad narudžba pređe u NA_PUTU. Servis za SMS ima metodu posalji(broj_telefona, tekst).
Pretpostavimo da klasa koja vodi narudžbu dobija SMS servis kao parametar:
def test_sms_kad_je_na_putu():
sms = Mock()
vodic = VodicNarudzbe(sms, telefon="+387 61 000 000")
vodic.promijeni("N-1001", "NA_PUTU")
sms.posalji.assert_called_once_with("+387 61 000 000", "Narudžba N-1001 je na putu")Telefon u testu je izmišljen. Bolje je koristiti create_autospec(SmsServis, instance=True) ako klasa postoji, da se uhvati pogrešno ime metode.
Funkcija rok_dostave() vraća vrijeme za 45 minuta od sada (sada() + 45 minuta). Kako testirati da narudžba u 22:30 ima rok u 23:15?
Zamijeniti sada fiksnim vremenom: with patch("modul.sada", return_value=dt.datetime(2026, 10, 22, 22, 30)): pa assert rok_dostave() == dt.datetime(2026, 10, 22, 23, 15). Dodati i granicu iza ponoći: 23:30 → 00:15 sljedećeg dana.
Test za funkciju ukupno mockuje cijena_dostave (vraća 300) i popust (vraća 0) i provjeri da je rezultat 2450 za N-1001. Šta nije u redu?
cijena_dostave i popust su naša brza pravila bez spoljnih zavisnosti; mockovanjem test više ne provjerava da su pravila tačna, nego samo da ukupno sabira tri broja. Greška D-02 (round umjesto ceil) bi prošla, jer mock uvijek vraća 300. Treba koristiti prave funkcije, a mockovati samo spore, skupe ili tuđe dijelove.
moze_otkazati u narudžbi za preuzimanje pada s KeyError u stanju SPREMNA.unittest.mock pravi dvojnike koji glume odgovore i kvarove i provjeravaju pozive, create_autospec čuva ugovor prave klase, patch zamjenjuje i trenutno vrijeme, a mockuje se samo ono što je sporo, skupo, nepouzdano ili tuđe.Narudzba.return_value, side_effect i assert_called_once_with?create_autospec bolji od običnog Mock()?patch? Kako je testirano radno vrijeme Picerije?radno_vrijeme.py izdvojeno u funkciju sada?