Testiranje softvera · Poglavlje 11 · Sedmica 11

Objektno orijentisano testiranje

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

Šta učimo ovog semestra

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

Priča za početak

Priča za početak

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.

Slika 11.1 – Therac-25: preuzet softver bez hardverske brave na koju je ranije računao
Slika 11.1 – Therac-25: preuzet softver bez hardverske brave na koju je ranije računao

Zašto ovo poglavlje

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.

Riječi ovog poglavlja (1/2)

  • Objekat i klasa – klasa je šablon (npr. Narudzba), objekat je jedan primjerak s vlastitim podacima (narudžba N-1001).
  • Stanje objekta – vrijednosti koje objekat trenutno čuva (stanje „U_PRIPREMI”, istorija prelaza).
  • Enkapsulacija – objekat skriva svoje podatke i dozvoljava promjenu samo kroz svoje metode.
  • Invarijanta klase – tvrdnja koja mora biti tačna za svaki objekat poslije svake javne metode (npr. posljednji element istorije je trenutno stanje).
  • Nasljeđivanje – nova klasa (podklasa) preuzima atribute i metode postojeće (roditelja) i mijenja ili dodaje neke.
  • Polimorfizam – isti poziv (n.promijeni(...)) radi različito zavisno od vrste objekta.

Riječi ovog poglavlja (2/2)

  • Liskovin princip zamjene – objekat podklase mora moći zamijeniti objekat roditelja svuda, a da program i dalje radi ispravno.
  • Ugovorni testovi roditelja – testovi napisani za roditelja koji se izvršavaju i nad svakom podklasom.
  • Mock – test dvojnik koji bilježi pozive i omogućava da test provjeri kako je pozvan.
  • create_autospec – pravi mock koji ima samo metode i parametre prave klase.
  • patch – privremeno zamijeni ime u nekom modulu dvojnikom, samo za vrijeme testa.
  • Trka podataka (race condition) – greška koja se javlja samo pri određenom redoslijedu ili brzini događaja; detaljno u poglavlju 12.

Ciljevi poglavlja

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.

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

ČasSadržajNa času
Teorija 111.1–11.3 Šta je posebno u objektima; stanje i nizovi metoda; invarijantaIstorija mora završavati trenutnim stanjem
Teorija 211.4–11.6 Nasljeđivanje; Liskovin princip; ugovorni testovi za podklase; polimorfizamNarudžba za preuzimanje i KeyError
Teorija 311.7–11.9 unittest.mock, create_autospec, patch; kad mockova ima previšeBanka ne odgovara
Dio 1 · Sedmica 11

Dio 1 – Klasa i stanje

11.1 Šta je posebno u objektima

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:

11.1 Šta je posebno u objektima

OsobinaIzazov za testeraU DeliveryApp
stanjerezultat metode zavisi od prethodnih poziva; testiraju se nizovi metodapromijeni("OTKAZANA") radi u NOVA, a ne radi u U_PRIPREMI
enkapsulacijapodaci su skriveni; test ih vidi samo kroz javne metode i atributestanje se mijenja samo kroz promijeni, ne direktno
nasljeđivanjepodklasa preuzima metode koje su testirane u drugom kontekstuNarudzbaZaPreuzimanje (11.4)
polimorfizamisti poziv radi različito za različite klase; svaka varijanta treba testn.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.

Ukratko – 11.1 Šta je posebno u objektima

  • Objekat ima stanje, pa rezultat metode zavisi od prethodnih poziva, kao kod automata za kafu.
  • Klasa se zato testira nizovima metoda, a ne samo pojedinačnim pozivima.
  • Enkapsulacija znači da test vidi objekat kroz javne metode.
  • Nasljeđivanje i polimorfizam traže da se ponašanje provjeri za svaku vrstu objekta.

11.2 Invarijanta klase

Neke stvari o narudžbi moraju biti tačne uvijek, bez obzira kroz koje je metode prošla:

  1. istorija počinje stanjem NOVA;
  2. posljednji element istorije je trenutno stanje;
  3. u istoriji nema dva ista stanja zaredom (nema „prelaza” iz stanja u isto stanje).

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:

11.2 Invarijanta klase

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"]

11.2 Invarijanta klase

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.

Ukratko – 11.2 Invarijanta klase

  • Invarijanta klase je tvrdnja koja mora važiti za svaki objekat poslije svake javne metode.
  • Za narudžbu: istorija počinje sa NOVA, završava trenutnim stanjem i nema ponovljenih stanja zaredom.
  • Invarijanta se piše jednom i provjerava poslije svake metode, posebno poslije neuspjele.
  • Neuspjela metoda ne smije ostaviti objekat napola promijenjen.

11.3 Testiranje kroz javni interfejs

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")

11.3 Testiranje kroz javni interfejs

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.

Ukratko – 11.3 Testiranje kroz javni interfejs

  • Objekat se testira kroz javni interfejs, onako kako ga koriste drugi dijelovi programa.
  • Postavljanje stanja „na silu” pravi objekte koji u stvarnom programu ne mogu postojati.
  • Objekat se u traženo stanje dovodi dozvoljenim putem, npr. fixtureom.
  • Testovi kroz javni interfejs ne pucaju kad se promijeni unutrašnjost, a ponašanje ostane isto.

Provjeri se

  1. Šta je invarijanta klase? Tvrdnja koja mora važiti za svaki objekat poslije svake javne metode.
  2. Zašto se invarijanta provjerava i poslije neuspjele metode? Da se provjeri da metoda koja javi grešku nije ostavila objekat napola promijenjen.
  3. Zašto je loše u testu napisati 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.

Dio 2 · Sedmica 11

Dio 2 – Nasljeđivanje i polimorfizam

11.4 Naslijeđena metoda u novom kontekstu

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:

11.4 Naslijeđena metoda u novom kontekstu

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)

11.4 Naslijeđena metoda u novom kontekstu

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'
Slika 11.2 – Naslijeđena metoda gleda podatke roditelja, u kojima novo stanje ne postoji
Slika 11.2 – Naslijeđena metoda gleda podatke roditelja, u kojima novo stanje ne postoji

11.4 Naslijeđena metoda u novom kontekstu

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

Za radoznale

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.

Ukratko – 11.4 Naslijeđena metoda u novom kontekstu

  • Naslijeđena metoda je testirana u kontekstu roditelja, a u podklasi se kontekst mijenja.
  • moze_otkazati naslijeđena u narudžbi za preuzimanje pada s KeyError u stanju SPREMNA.
  • Naslijeđene metode moraju se ponovo testirati u podklasi, iako nijedan red nije promijenjen.
  • Therac-25 je ista greška u velikom: preuzet kod izgubio je zaštitu iz starog konteksta.

11.5 Liskovin princip i ugovorni testovi roditelja

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:

  • nova narudžba je u stanju NOVA i može se otkazati;
  • iz PRIHVAĆENA se može otkazati;
  • iz U_PRIPREMI se ne može otkazati (PP-6);
  • 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:

11.5 Liskovin princip i ugovorni testovi roditelja

@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 stanju

11.5 Liskovin princip i ugovorni testovi roditelja

pytest -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] PASSED

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

Ukratko – 11.5 Liskovin princip i ugovorni testovi roditelja

  • Liskovin princip: objekat podklase mora moći zamijeniti objekat roditelja bilo gdje, a program i dalje radi ispravno.
  • Ugovor roditelja su pravila koja moraju važiti za svaku podklasu, npr. PP-6.
  • Parametrizovan fixture izvršava iste ugovorne testove nad svakom podklasom.
  • Nova podklasa se samo doda u listu params i odmah prolazi kroz sve ugovorne testove.

11.6 Polimorfizam

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:

  1. Za svaku podklasu, svaka zamijenjena metoda ima svoje testove (npr. test_preuzimanje_nema_dostave: narudžba za preuzimanje ne smije ići u NA_PUTU).
  2. Za svaku podklasu, ugovorni testovi roditelja (11.5).
  3. Za svako mjesto koje koristi polimorfni poziv, bar po jedan test sa svakom vrstom objekta (dugme „Otkaži” s narudžbom za dostavu i s narudžbom za preuzimanje).

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.

11.6 Polimorfizam

Česta zabluda

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

Ukratko – 11.6 Polimorfizam

  • Polimorfizam znači da isti poziv radi različito za različite vrste objekata.
  • Svaka zamijenjena metoda podklase dobija svoje testove, a svaka podklasa i ugovorne testove roditelja.
  • Mjesto koje koristi polimorfni poziv treba testirati sa svakom vrstom objekta.
  • Nova metoda u podklasi može promijeniti ponašanje i naslijeđenih metoda.

Provjeri se

  1. Zašto se naslijeđena metoda ponovo testira u podklasi? Jer se promijenio kontekst (podaci, stanja, druge metode), pa ispravna metoda može pasti, kao moze_otkazati sa SPREMNA.
  2. Šta kaže Liskovin princip zamjene? Objekat podklase mora moći zamijeniti objekat roditelja bilo gdje, a program i dalje radi ispravno.
  3. Kako pytest izvršava iste testove za više podklasa? Parametrizovanim fixtureom s listom klasa u params.

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

Dio 3 · Sedmica 11

Dio 3 – Mock i patch

11.7 unittest.mock

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:

11.7 unittest.mock

ŠtaKakoPrimjer
napraviti dvojnikaMock() ili create_autospec(Klasa, instance=True)placanje = create_autospec(PlacanjeServis, instance=True)
zadati povratnu vrijednost.return_valueplacanje.naplati.return_value = "T-0042"
zadati izuzetak.side_effectplacanje.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_countkoliko 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")

11.7 unittest.mock

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 napravljena

Banka „ne odgovara”, a test provjerava da narudžba nije napravljena: kupac ne smije dobiti potvrdu za neplaćenu narudžbu.

Ukratko – 11.7 unittest.mock

  • Modul 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.
  • Mock omogućava da se glumi kvar, npr. banka koja ne odgovara, koji je s pravim servisom teško izazvati.

11.8 autospec i patch

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 klasi

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

11.8 autospec i patch

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 < do

Kako 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 h

11.8 autospec i patch

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

Na času

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.

Ukratko – 11.8 autospec i patch

  • Obični 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.
  • Trenutno vrijeme i slične zavisnosti treba izdvojiti u posebne funkcije da bi bile testabilne.

11.9 Kad mockova ima previše

Mockovi su moćni, i zato opasni. Tri znaka da ih je previše:

  1. Test provjerava kako, a ne šta. Test koji provjerava da je 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).
  2. Mockuje se ono što je naše i brzo. Pravila DeliveryApp (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.
  3. Test više liči na kod nego na zahtjev. Ako test od 30 redova podešava 8 mockova da bi provjerio jednu liniju, vjerovatno je dizajn previše zapetljan; to je signal za refaktorisanje, ne za još mockova.

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.

11.9 Kad mockova ima previše

Iz stvarnog svijeta

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.

Ukratko – 11.9 Kad mockova ima previše

  • Previše mockova čini testove krhkim: provjeravaju kako kod radi umjesto šta postiže.
  • Mockuje se ono što je sporo, skupo, nepouzdano ili tuđe, a ne naša brza pravila.
  • Test s mnogo mockova za malo koda je znak da dizajn treba pojednostaviti.
  • Testovi s mockovima moraju biti dopunjeni integracionim i sistemskim testovima.

Provjeri se

  1. Šta radi side_effect? Zada izuzetak (ili niz odgovora) koji mock javlja kad se pozove, npr. ConnectionError za banku koja ne odgovara.
  2. Zašto je create_autospec sigurniji od Mock()? Jer ima samo metode i parametre prave klase, pa pogrešno ime metode odmah javi AttributeError.
  3. Šta treba mockovati, a šta ne? Mockovati sporo, skupo, nepouzdano ili tuđe (banka, mapa, sat); naša brza pravila koristiti prava.

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

Praksa · Sedmica 11

Praktična nastava – Objekti, podklase i mockovi

Praktična nastava – Objekti, podklase i mockovi

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.

Korak 1 – Invarijanta (30 minuta)

  1. Pokrenite pytest -v tests/test_oo.py nad ispravnom i nad prvom verzijom; zapišite rezultate.
  2. U svojoj kopiji 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?
  3. Dodajte poziv provjeri_invarijantu(n) poslije svakog koraka u testu modela iz poglavlja 9 (tests/test_model.py) i pokrenite ga nad obje verzije.

Korak 2 – Naslijeđena greška (30 minuta)

  1. U kopiji 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?
  2. Popravite dizajn: u svojoj kopiji 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.

Korak 3 – Nova podklasa (45 minuta)

  1. Napravite podklasu NarudzbaZaFirmu: firma plaća mjesečno, pa nakon ISPORUČENA ide stanje FAKTURISANA. Napišite tabelu prelaza iz specifikacije, prije koda.
  2. Dodajte je u 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.)
  3. Napišite bar dva testa za ponašanje koje je samo u novoj podklasi.

Korak 4 – Mock i patch (45 minuta)

  1. Napišite test u kojem servis mapa javlja KeyError (nepoznata adresa). Šta treba da se desi s narudžbom? Da li je banka pozvana? Provjerite sa assert_not_called().
  2. Napišite test u kojem banka prvi put javi 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.
  3. Testirajte radno vrijeme Roštilja (R-2) i Salatarnice (R-3) na svim granicama, sa patch.
  4. Napišite namjerno „loš” test s mockom koji provjerava da je 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.

Vježbe

Vježbe za studenta

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

Vježba za studenta 1 – invarijanta.

Zadatak

Klasa Korpa ima listu stavke (parovi cijena, količina) i atribut ukupno, koji se ažurira u metodama dodaj i ukloni. Napišite invarijantu.

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

Rješenje (R)

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

Vježba za studenta 2 – Liskov.

Zadatak

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?

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

Rješenje (R)

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.

Vježba za studenta 3 – mock.

Zadatak

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

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

Rješenje (R)

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.

Vježba za studenta 4 – patch.

Zadatak

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?

Vježba za studenta 4 – patch. – rješenje

Rješenje (R)

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.

Vježba za studenta 5 – previše mockova.

Zadatak

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?

Vježba za studenta 5 – previše mockova. – rješenje

Rješenje (R)

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.

Sažetak u pet rečenica

  1. Therac-25 je 1985–1987. davao smrtonosne doze zračenja jer je softver preuzet sa starijih modela izgubio hardversku bravu na koju je računao, a stanje u memoriji se pri brzom unosu nije slagalo sa stanjem mašine.
  2. Objekti imaju stanje, pa se testiraju nizovima metoda kroz javni interfejs, a invarijanta klase, npr. da istorija završava trenutnim stanjem, provjerava se poslije svake metode, i uspjele i neuspjele.
  3. Naslijeđena metoda mora se ponovo testirati u podklasi, jer se mijenja kontekst, kao što moze_otkazati u narudžbi za preuzimanje pada s KeyError u stanju SPREMNA.
  4. Liskovin princip traži da podklasa može zamijeniti roditelja, a parametrizovan fixture izvršava iste ugovorne testove roditelja nad svakom podklasom.
  5. Modul 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.

Pitanja za ponavljanje

  1. Šta se desilo s aparatom Therac-25 i koje su dvije glavne pouke za testere?
  2. Po čemu se testiranje objekata razlikuje od testiranja funkcija?
  3. Šta je invarijanta klase? Navedite invarijantu klase Narudzba.
  4. Zašto se invarijanta provjerava i poslije neuspjele metode?
  5. Zašto se objekat testira kroz javni interfejs, a ne postavljanjem unutrašnjih podataka?
  6. Zašto naslijeđena metoda mora biti ponovo testirana u podklasi? Navedite primjer iz DeliveryApp.
  7. Šta kaže Liskovin princip zamjene?
  8. Kako se u pytestu isti testovi izvršavaju nad svim podklasama?
  9. Šta je polimorfizam i šta traži od testera?
  10. Šta rade return_value, side_effect i assert_called_once_with?
  11. Zašto je create_autospec bolji od običnog Mock()?
  12. Čemu služi patch? Kako je testirano radno vrijeme Picerije?
  13. Zašto je trenutno vrijeme u radno_vrijeme.py izdvojeno u funkciju sada?
  14. Koji su znaci da test ima previše mockova?