Objects have state worth testing.
A function receives data and returns a result. An object also has state: its methods can change attributes over time. That means a useful test often checks the state before and after an operation, not only a return value.
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)The test creates a Brojac, calls its method, then checks the changed attribute. The behavior we care about is the object state: vrednost should become 1.
For a class, ask three useful questions: What is the initial state? What state should follow one operation? What should happen after another operation? Each test should create or receive the state it needs instead of relying on a previous test.
Test the rule, not just the method call: A method can run without an exception and still leave an object in the wrong state. Assertions make the expected rule visible.
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"])These tests use separate objects. One checks an empty basket, while the other checks a changed basket. No test needs another test to run first.
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() runs automatically before every test method. It is a helper method on the test class, not the constructor of Korpa. Use it when several tests need similar preparation; each test still receives a fresh object, so test order does not decide correctness.
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 rejects an invalid value deliberately. Inside with self.assertRaises(ValueError), that expected exception makes the assertion pass. If no exception appears, the test FAILs. If a different unexpected exception appears, the test can ERROR. Type hints describe expected types, but do not validate values during execution.
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)This test checks two separate facts: the operation raises ValueError, and the wallet keeps its old balance. assertRaises() verifies only the exception; the later assertEqual verifies state. A production wallet would also validate negative amounts, but this small example stays focused on insufficient funds.
# 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)Normal inputs show ordinary behavior. Edge cases check a boundary, such as an amount exactly equal to a balance. Invalid inputs check that the class follows a clear rule when an operation must be rejected.
# 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)A test should be able to run alone. Do not use one shared instance whose state an earlier test changes. Fresh setup makes a failing test easier to understand and prevents hidden dependencies on execution order.
Connect the testing tools you know
- Describe object rules clearlyOOP gives us classes, objects, methods, and state. Type hints and docstrings help explain what a method expects and promises.
- Use unittest to protect those rulesAssertions check expected state and results. Expected exceptions test deliberate safeguards instead of treating every raised exception as an ERROR.
- Investigate a failing test carefullyDebugging helps find a cause after a test reports a problem. Together, clear code and focused tests make future maintenance safer.
Common class-testing mistakes
- Sharing one mutable object between testsUse a local object or setUp() so one test cannot accidentally change the starting state for another.
- Treating every exception as an ERRORAn expected ValueError can be correct behavior when a rule rejects invalid input. assertRaises() must name the specific exception you expect.
- Checking only that an exception happenedAfter a rejected operation, add a separate assertion for the object state when that state must remain safe.
- Assuming setUp() is the tested class constructorsetUp() belongs to the test class and prepares each test. It does not replace __init__ on the class you are testing.
