Yesterday: tests are functions with assert. Today: the standard-library framework that organises them.
import unittest
from dataclasses import dataclass
@dataclass
class Item:
name: str
value: int
def is_high(self):
return self.value > 10
class TestItem(unittest.TestCase):
def test_creation(self):
a = Item("a", 5)
self.assertEqual(a.name, "a")
self.assertEqual(a.value, 5)
def test_is_high_true(self):
self.assertTrue(Item("b", 12).is_high())
def test_is_high_false(self):
self.assertFalse(Item("a", 5).is_high())
result = unittest.main(argv=[''], exit=False, verbosity=0)
print("ran:", result.result.testsRun, "failures:", len(result.result.failures))Each test is a method on a TestCase class?
Right. The framework discovers every method whose name starts with test_ and runs each as a separate test. Failures in one don't stop the others — you find out about all the broken behaviours at once, not just the first one.
What's self.assertEqual instead of plain assert?
Better failure messages. assert a.value == 5 says AssertionError and stops. self.assertEqual(a.value, 5) says 5 != 6 (or whatever the actual value was) and you immediately know what went wrong without re-running.
And unittest.main(argv=[''], exit=False) — why those arguments?
Defaults are designed for command-line python -m unittest. To run inside another script (like a notebook or this exercise), pass argv=[''] (no test names from the command line) and exit=False (don't sys.exit() when done — let the script keep running). Real codebases run python -m unittest discover from the shell instead.
unittest — standard-library test frameworkimport unittest
class TestThing(unittest.TestCase):
def test_one(self):
self.assertEqual(1 + 1, 2)
def test_two(self):
self.assertTrue("hi".startswith("h"))
def test_raises(self):
with self.assertRaises(ZeroDivisionError):
1 / 0
if __name__ == "__main__":
unittest.main()| Piece | Purpose |
|---|---|
class TestThing(unittest.TestCase): | a group of related tests |
def test_one(self): | a single test (any method named test_*) |
self.assertEqual(a, b) | fail with both values shown if a != b |
with self.assertRaises(Cls): | fail unless the block raises Cls |
unittest.main() | discover and run everything |
| Method | Passes when |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNone(x) | x is None |
assertIn(x, container) | x in container |
assertRaises(Cls) | block raises Cls |
assertAlmostEqual(a, b) | a and b close (for floats) |
class TestX(unittest.TestCase):
def setUp(self):
self.fixture = expensive_setup()
def test_one(self):
self.fixture.do_thing()
self.assertEqual(self.fixture.state, ...)
def tearDown(self):
self.fixture.cleanup()Every test_* method gets a fresh setUp call before it runs. If tearDown is defined, it runs after. Useful for resetting state between tests.
class TestThree(unittest.TestCase):
def test_a(self):
self.assertEqual(1, 1) # passes
def test_b(self):
self.assertEqual(1, 2) # FAILS
def test_c(self):
self.assertEqual(1, 1) # still runs and passesThe failure of test_b doesn't stop test_c. The runner reports 2 passed, 1 failed.
From a script you'd usually run:
python -m unittest discoverWhich finds every test_*.py file and runs every TestCase inside. From inside a notebook or another script, use:
result = unittest.main(argv=[''], exit=False, verbosity=0)argv=[''] skips command-line argument parsing; exit=False lets the script keep going after the tests run. The result.result object exposes testsRun, failures, and errors lists.
pytestMost real Python codebases use pytest instead. The mental model is identical — write functions/methods that assert. The syntax differs (plain assert instead of self.assertEqual, fixtures via decorators). When you switch, the translation is mechanical.
Yesterday: tests are functions with assert. Today: the standard-library framework that organises them.
import unittest
from dataclasses import dataclass
@dataclass
class Item:
name: str
value: int
def is_high(self):
return self.value > 10
class TestItem(unittest.TestCase):
def test_creation(self):
a = Item("a", 5)
self.assertEqual(a.name, "a")
self.assertEqual(a.value, 5)
def test_is_high_true(self):
self.assertTrue(Item("b", 12).is_high())
def test_is_high_false(self):
self.assertFalse(Item("a", 5).is_high())
result = unittest.main(argv=[''], exit=False, verbosity=0)
print("ran:", result.result.testsRun, "failures:", len(result.result.failures))Each test is a method on a TestCase class?
Right. The framework discovers every method whose name starts with test_ and runs each as a separate test. Failures in one don't stop the others — you find out about all the broken behaviours at once, not just the first one.
What's self.assertEqual instead of plain assert?
Better failure messages. assert a.value == 5 says AssertionError and stops. self.assertEqual(a.value, 5) says 5 != 6 (or whatever the actual value was) and you immediately know what went wrong without re-running.
And unittest.main(argv=[''], exit=False) — why those arguments?
Defaults are designed for command-line python -m unittest. To run inside another script (like a notebook or this exercise), pass argv=[''] (no test names from the command line) and exit=False (don't sys.exit() when done — let the script keep running). Real codebases run python -m unittest discover from the shell instead.
unittest — standard-library test frameworkimport unittest
class TestThing(unittest.TestCase):
def test_one(self):
self.assertEqual(1 + 1, 2)
def test_two(self):
self.assertTrue("hi".startswith("h"))
def test_raises(self):
with self.assertRaises(ZeroDivisionError):
1 / 0
if __name__ == "__main__":
unittest.main()| Piece | Purpose |
|---|---|
class TestThing(unittest.TestCase): | a group of related tests |
def test_one(self): | a single test (any method named test_*) |
self.assertEqual(a, b) | fail with both values shown if a != b |
with self.assertRaises(Cls): | fail unless the block raises Cls |
unittest.main() | discover and run everything |
| Method | Passes when |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a, b) | a is b |
assertIsNone(x) | x is None |
assertIn(x, container) | x in container |
assertRaises(Cls) | block raises Cls |
assertAlmostEqual(a, b) | a and b close (for floats) |
class TestX(unittest.TestCase):
def setUp(self):
self.fixture = expensive_setup()
def test_one(self):
self.fixture.do_thing()
self.assertEqual(self.fixture.state, ...)
def tearDown(self):
self.fixture.cleanup()Every test_* method gets a fresh setUp call before it runs. If tearDown is defined, it runs after. Useful for resetting state between tests.
class TestThree(unittest.TestCase):
def test_a(self):
self.assertEqual(1, 1) # passes
def test_b(self):
self.assertEqual(1, 2) # FAILS
def test_c(self):
self.assertEqual(1, 1) # still runs and passesThe failure of test_b doesn't stop test_c. The runner reports 2 passed, 1 failed.
From a script you'd usually run:
python -m unittest discoverWhich finds every test_*.py file and runs every TestCase inside. From inside a notebook or another script, use:
result = unittest.main(argv=[''], exit=False, verbosity=0)argv=[''] skips command-line argument parsing; exit=False lets the script keep going after the tests run. The result.result object exposes testsRun, failures, and errors lists.
pytestMost real Python codebases use pytest instead. The mental model is identical — write functions/methods that assert. The syntax differs (plain assert instead of self.assertEqual, fixtures via decorators). When you switch, the translation is mechanical.
Create a free account to get started. Paid plans unlock all tracks.