Objekti imaju stanje koje vredi testirati.
Funkcija prima podatke i vraća rezultat. Objekat takođe ima stanje: njegove metode mogu menjati atribute tokom vremena. Zato koristan test često proverava stanje pre i posle operacije, a ne samo povratnu vrednost.
class Brojac:
def __init__(self):
self.vrednost = 0
def povecaj(self):
self.vrednost += 1
import unittest
class TestBrojac(unittest.TestCase):
def test_povecanje(self):
brojac = Brojac()
brojac.povecaj()
self.assertEqual(brojac.vrednost, 1)Test pravi Brojac, poziva njegov metod, a zatim proverava izmenjeni atribut. Ponašanje koje nas zanima je stanje objekta: vrednost treba da postane 1.
Za klasu postavi tri korisna pitanja: Kakvo je početno stanje? Koje stanje treba da sledi nakon jedne operacije? Šta treba da se dogodi nakon druge operacije? Svaki test treba da napravi ili dobije stanje koje mu je potrebno, umesto da se oslanja na prethodni test.
Testiraj pravilo, a ne samo poziv metoda: Metod može da se izvrši bez izuzetka, a ipak ostavi objekat u pogrešnom stanju. Provere čine očekivano pravilo vidljivim.
class Korpa:
def __init__(self):
self.proizvodi = []
def dodaj(self, proizvod: str):
self.proizvodi.append(proizvod)
class TestKorpa(unittest.TestCase):
def test_pocetno_stanje(self):
korpa = Korpa()
self.assertEqual(korpa.proizvodi, [])
def test_dodavanje_dva_proizvoda(self):
korpa = Korpa()
korpa.dodaj("Knjiga")
korpa.dodaj("Olovka")
self.assertEqual(korpa.proizvodi, ["Knjiga", "Olovka"])Ovi testovi koriste odvojene objekte. Jedan proverava praznu korpu, dok drugi proverava izmenjenu korpu. Nijednom testu nije potrebno da se drugi test prethodno pokrene.
import unittest
class TestKorpa(unittest.TestCase):
def setUp(self):
self.korpa = Korpa()
def test_pocetno_stanje(self):
self.assertEqual(self.korpa.proizvodi, [])
def test_dodavanje(self):
self.korpa.dodaj("Knjiga")
self.assertEqual(self.korpa.proizvodi, ["Knjiga"])setUp() se automatski pokreće pre svake test metode. To je pomoćni metod test klase, a ne konstruktor klase Korpa. Koristi ga kada je većem broju testova potrebna slična priprema; svaki test i dalje dobija svež objekat, zato redosled testova ne određuje ispravnost.
def postavi_godine(godine: int) -> int:
if godine < 0:
raise ValueError("Godine ne mogu biti negativne.")
return godine
class TestGodine(unittest.TestCase):
def test_negativne_godine(self):
with self.assertRaises(ValueError):
postavi_godine(-5)raise ValueError namerno odbija neispravnu vrednost. Unutar with self.assertRaises(ValueError) taj očekivani izuzetak čini da provera prođe. Ako se izuzetak ne pojavi, test daje FAIL. Ako se pojavi drugi, neočekivani izuzetak, test može dati ERROR. Oznake tipova opisuju očekivane tipove, ali ne proveravaju vrednosti tokom izvršavanja.
class Novcanik:
def __init__(self, stanje: float):
self.stanje = stanje
def potrosi(self, iznos: float):
if iznos > self.stanje:
raise ValueError("Nema dovoljno novca.")
self.stanje -= iznos
def test_nedovoljno_sredstava(self):
novcanik = Novcanik(100)
with self.assertRaises(ValueError):
novcanik.potrosi(150)
self.assertEqual(novcanik.stanje, 100)Ovaj test proverava dve odvojene činjenice: operacija podiže ValueError, a novčanik zadržava prethodno stanje. assertRaises() proverava samo izuzetak; kasniji assertEqual proverava stanje. Pravi novčanik bi proveravao i negativne iznose, ali ovaj mali primer ostaje fokusiran na nedovoljno novca.
# Normal: deposit 100 RSD
racun.uplati(100)
# Edge: withdraw exactly the available balance
racun.isplati(racun.stanje)
# Invalid: withdraw more than the balance
with self.assertRaises(ValueError):
racun.isplati(racun.stanje + 1)Uobičajeni ulazi pokazuju redovno ponašanje. Rubni slučajevi proveravaju granicu, kao što je iznos tačno jednak stanju. Neispravni ulazi proveravaju da li klasa sledi jasno pravilo kada operacija mora biti odbijena.
# Avoid one shared object changed by earlier tests
class TestRacun(unittest.TestCase):
def setUp(self):
self.racun = BankovniRacun(100)
def test_uplata(self):
self.racun.uplati(50)
self.assertEqual(self.racun.stanje, 150)Test treba da može da se pokrene samostalno. Nemoj koristiti jednu deljenu instancu čije stanje prethodni test menja. Sveža priprema olakšava razumevanje neuspešnog testa i sprečava skrivene zavisnosti od redosleda izvršavanja.
Poveži alate za testiranje koje poznaješ
- Jasno opiši pravila objektaOOP nam daje klase, objekte, metode i stanje. Oznake tipova i docstringovi pomažu da se objasni šta metod očekuje i šta obećava.
- Koristi unittest da zaštitiš ta pravilaProvere testiraju očekivano stanje i rezultate. Očekivani izuzeci testiraju namerno postavljene zaštite, umesto da se svaki podignuti izuzetak smatra ERROR rezultatom.
- Pažljivo istraži neuspešan testDebagovanje pomaže da pronađeš uzrok nakon što test prijavi problem. Zajedno, jasan kod i fokusirani testovi čine buduće održavanje bezbednijim.
Česte greške pri testiranju klasa
- Deljenje jednog promenljivog objekta između testovaKoristi lokalni objekat ili setUp() da jedan test ne bi slučajno promenio početno stanje za drugi test.
- Smatranje svakog izuzetka za ERROROčekivani ValueError može biti ispravno ponašanje kada pravilo odbija neispravan ulaz. assertRaises() mora navesti konkretan izuzetak koji očekuješ.
- Provera samo da se izuzetak dogodioNakon odbijene operacije dodaj zasebnu proveru stanja objekta kada to stanje mora ostati bezbedno.
- Pretpostavka da je setUp() konstruktor testirane klasesetUp() pripada test klasi i priprema svaki test. Ne zamenjuje __init__ u klasi koju testiraš.
