PYTHON / TESTING AND TOOLING
unittest basics
Write and run stdlib unittest tests: TestCase subclasses, test_ methods, assertEqual/assertRaises, setUp isolation, subTest, and reading the report.
What you will learn
- Collect tests as test_-prefixed methods on a unittest.TestCase subclass
- Build per-test state in setUp; unittest re-instantiates the class for each test
- Assert with self.assertEqual/assertRaises so the message shows the real values
- Run a suite with python -m unittest -v and read failures vs errors separately
Understanding unittest basics
unittest is the xUnit-style framework in the standard library, so it is always available with no install step. A test is a method whose name starts with test, defined on a subclass of unittest.TestCase; the loader inspects the class, finds those methods, and wraps each one in its own test object. The key mechanical detail is that unittest constructs a separate instance of your TestCase class for every test method it found, so attributes you set in one test cannot be seen by another. That is the whole isolation model: fresh object, fresh attributes.
Checks are made through methods on self rather than the bare assert statement, because TestCase needs to control the message and to distinguish an assertion failure (reported as FAIL) from an unexpected exception (reported as ERROR). self.assertEqual(a, b) knows both operands, so a mismatch prints them; self.assertTrue(a == b) only sees a boolean and can print nothing more useful than 'False is not true'. Exception behaviour has its own assertion: with self.assertRaises(ValueError): ... passes only if the block raises ValueError, and it re-raises anything else so you never accidentally swallow a different bug.
Fixtures hang off named hooks. setUp runs before every test method and tearDown after it, even if the test failed; setUpClass and tearDownClass are classmethods that run once around the whole class, which is why anything mutable created there leaks between tests. Within a class, methods run in alphabetical order by name, but you should never depend on that: since each test gets its own instance, sequences like test_1_create followed by test_2_read do not share state and will fail when run individually. To run everything, use python -m unittest (or python -m unittest discover), which imports files matching test*.py from the current directory and collects every TestCase it finds.
import unittest
def normalize_tag(tag):
if not isinstance(tag, str):
raise TypeError("tag must be a string")
cleaned = "-".join(tag.strip().lower().split())
if not cleaned:
raise ValueError("tag must not be empty")
return cleaned
class NormalizeTagTests(unittest.TestCase):
def test_collapses_whitespace_and_lowercases(self):
self.assertEqual(normalize_tag(" Unit Testing "), "unit-testing")
def test_blank_input_raises_value_error(self):
with self.assertRaises(ValueError):
normalize_tag(" ")
def test_non_string_raises_type_error(self):
with self.assertRaises(TypeError):
normalize_tag(42)
# In a real file this is: if __name__ == "__main__": unittest.main()
unittest.main(argv=["tests"], exit=False)A unittest test is a test_-prefixed method running on its own fresh TestCase instance, and it signals failure through self.assert* rather than a bare assert.
Worked examples
setUp runs once per test, not once per class
Records the fixture hooks to show that each test method gets a fresh instance and a fresh setUp call.
import io
import unittest
class Counter:
def __init__(self):
self.value = 0
def bump(self):
self.value += 1
return self.value
calls = []
class CounterTests(unittest.TestCase):
def setUp(self):
calls.append("setUp")
self.counter = Counter()
def tearDown(self):
calls.append("tearDown")
def test_bump_once(self):
self.assertEqual(self.counter.bump(), 1)
def test_bump_twice(self):
self.counter.bump()
self.assertEqual(self.counter.bump(), 2)
suite = unittest.TestLoader().loadTestsFromTestCase(CounterTests)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print("tests run:", result.testsRun)
print("successful:", result.wasSuccessful())
print("hooks:", calls)Example explained
Line 1loadTestsFromTestCase builds one test object per test_ method, each with its own CounterTests instance.
Line 2The hooks list is setUp/tearDown twice, proving setUp re-created self.counter for the second test.
Line 3test_bump_twice can assume value starts at 0 because it never sees the counter test_bump_once mutated.
Line 4Passing stream=io.StringIO() swallows the runner's report so only the printed summary appears.
subTest keeps a loop from stopping at the first bad case
Shows how subTest reports every failing input inside a single test method instead of aborting at the first assertion.
import unittest
def is_valid_port(n):
# Off-by-one bug at both ends of the range.
return isinstance(n, int) and 1 < n < 65535
class PortTests(unittest.TestCase):
def test_accepts_whole_range(self):
for port in (1, 80, 65535):
with self.subTest(port=port):
self.assertTrue(is_valid_port(port), f"port {port} should be accepted")
result = unittest.TestResult()
unittest.TestLoader().loadTestsFromTestCase(PortTests).run(result)
print("tests run:", result.testsRun)
print("failures:", len(result.failures))
for _test, traceback_text in result.failures:
print(traceback_text.strip().splitlines()[-1])Example explained
Line 1Without subTest, the failure on port 1 would raise and port 65535 would never be checked.
Line 2testsRun stays at 1 because subtests are reported against the single enclosing test method.
Line 3assertTrue only sees a boolean, so the standard message is 'False is not true'; the msg argument after ' : ' is what identifies the input.
Line 4Running the suite against a plain TestResult lets the script inspect result.failures instead of printing a report.
Asserting on exceptions and their messages
Uses assertRaises as a context manager to inspect the raised exception, and assertRaisesRegex to match its text.
import io
import unittest
def parse_port(text):
n = int(text)
if not 1 <= n <= 65535:
raise ValueError(f"port out of range: {n}")
return n
class ParsePortTests(unittest.TestCase):
def test_returns_int(self):
self.assertEqual(parse_port("8080"), 8080)
def test_out_of_range_message(self):
with self.assertRaises(ValueError) as ctx:
parse_port("70000")
self.assertIn("out of range", str(ctx.exception))
def test_non_numeric_input(self):
with self.assertRaisesRegex(ValueError, "invalid literal"):
parse_port("eighty")
result = unittest.TextTestRunner(stream=io.StringIO()).run(
unittest.TestLoader().loadTestsFromTestCase(ParsePortTests)
)
print(result.testsRun, "tests,", "OK" if result.wasSuccessful() else "FAILED")Example explained
Line 1The context manager form exposes ctx.exception after the block, so you can assert on the message separately from the type.
Line 2assertRaisesRegex applies re.search to str(exception), which is why the short fragment 'invalid literal' is enough.
Line 3Both tests would fail loudly if the call returned normally, so a silently-removed raise cannot slip past.
Line 4int() raising ValueError for 'eighty' is the same exception class as the range check, so the regex is what distinguishes the two paths.
Important notes
unittest writes its dots and its summary to stderr, so in editors that split the streams the report shows up in the error pane even when everything passes.
The old camelCase aliases (assertEquals, failUnless, assert_) were removed in Python 3.12 — use assertEqual, assertTrue and friends.
Common mistakes
Writing self.assertRaises(ValueError, normalize_tag("")) — the call happens before assertRaises sees it, so the exception escapes and the test is reported as an ERROR instead of passing; use the with-block or pass the callable and its arguments separately.
Naming a method check_blank_tag or validate_input instead of test_blank_tag: the loader only collects names starting with 'test', so it never runs and the summary quietly says 'Ran 2 tests' while you believe you wrote three.
Putting a mutable fixture in the class body (items = []) or in setUpClass and then mutating it: setUpClass runs once, so leftovers leak into later tests and results change depending on which tests you run.
Try it yourself
Change, predict, then run
Write a TestCase for a function initials(name) that turns "ada lovelace" into "A.L.": one test with assertEqual for the happy path, one with assertRaises(ValueError) for an empty string, and one that uses subTest to loop over three spellings. Run it with unittest.main(argv=["x"], exit=False) and confirm the summary says Ran 3 tests.
Open the Python workspaceCheck your understanding
A TestCase sets self.items = [] in setUp. test_appends_one appends to self.items and asserts its length is 1; test_starts_empty asserts self.items is empty. Both pass. Why?
- unittest creates a new instance of the test class for each test method, so setUp assigns a brand-new list every time
- tearDown automatically undoes any attribute that setUp assigned
- self.items is deep-copied before each assertion, so mutations are discarded
- It only works because methods run alphabetically, so test_appends_one happens after test_starts_empty
Show answer
The loader builds one TestCase instance per test method, so setUp runs against a fresh object and the list from the first test is never reused. Option 4 is tempting because alphabetical ordering is real, but here test_appends_one actually runs first and test_starts_empty still passes — the isolation comes from re-instantiation, not ordering. tearDown does nothing at all unless you write it, and nothing is copied.