Unit Testing in Python with unittest: A Practical Introduction
- A test case is a class inheriting from unittest.TestCase, with each test as a method whose name starts with test_.
- assertEqual, assertTrue, assertRaises and similar methods report a clear, specific failure message when a check fails.
- setUp() runs before every test method in a class, giving each test a fresh, isolated starting state.
- unittest is in the standard library, so it runs with no installation on any Python 3 setup.
A first test case
A test case is a class that inherits from unittest.TestCase. Each method whose name starts with test_ is treated as an individual test and run independently:
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
if __name__ == "__main__":
unittest.main()
Running this file directly calls unittest.main(), which discovers every test_ method on every TestCase subclass in the module, runs them, and prints a summary — a dot for each pass, an F for each failure, with a full traceback for anything that fails.
The assert method family
TestCase provides dozens of assertion methods beyond assertEqual, each producing a more specific, more useful failure message than a bare assert statement would:
self.assertEqual(add(2, 3), 5)
self.assertNotEqual(add(2, 3), 6)
self.assertTrue(5 > 3)
self.assertFalse(3 > 5)
self.assertIn("a", "abc")
self.assertIsNone(None)
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
When assertEqual(add(2, 3), 5) fails, the message shows both the actual and expected values automatically — something a plain assert add(2, 3) == 5 doesn't give you without extra work. assertAlmostEqual matters more than it looks: floating-point arithmetic makes exact equality checks unreliable, so comparing to a fixed number of decimal places avoids tests that fail on rounding noise that has nothing to do with the code being wrong.
setUp and tearDown: repeated fixtures
Tests that need the same starting object shouldn't repeat its construction in every method. setUp() runs immediately before each test method, giving every test in the class a fresh instance:
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
self.cart.add_item("apple", 0.50)
def test_starts_with_one_item(self):
self.assertEqual(len(self.cart.items), 1)
def test_total_matches_item_price(self):
self.assertEqual(self.cart.total(), 0.50)
setUp() runs fresh before every single test method — the cart from test_starts_with_one_item is not the same object used in test_total_matches_item_price, which keeps tests from silently depending on each other's leftover state. tearDown() is the counterpart, running after every test, useful for closing files or connections a test opened.
Testing that an exception is raised
Confirming that bad input correctly raises an error is just as much a real test as checking a correct return value, and assertRaises handles it cleanly as a context manager:
def divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_divide_by_zero_raises(self):
with self.assertRaises(ValueError):
divide(10, 0)
The test passes only if a ValueError is actually raised inside the with block; if nothing is raised, or a different exception type is raised, the test fails with a message saying exactly that. This is the standard way to verify error-handling paths, which are easy to leave completely untested otherwise.
Running a whole test suite
Beyond a single file, unittest's discovery mode finds and runs every test across a project without listing files individually:
python -m unittest discover -s tests -p "test_*.py"
This walks the tests directory, imports every file matching test_*.py, collects every TestCase in them, and runs the lot, printing a single combined summary at the end. Most projects wire this into a Makefile target or CI step rather than typing it out by hand each time, but it's worth running manually at least once to see what a full suite's output actually looks like.
What actually makes a good first test
The temptation when starting out is to test trivial things — that 2 + 2 == 4, effectively — which pad a test count without catching real bugs. Worthwhile first tests target the edges: an empty list passed where a populated one is expected, a negative number where the function assumed positive, a duplicate key inserted into something that's supposed to reject duplicates. A function with three inputs and no branches barely needs a test; a function with an if, a loop, and a special case for zero needs at least one test per path through it. Coverage percentage is a poor proxy for this — a suite that exercises every line but never checks a boundary condition can still miss the bug that actually ships. The full list of assertion methods is in the official unittest documentation.
Naming tests matters more than it seems like it should once a suite grows past a handful of files. A method called test_1 or test_case_a tells a future reader nothing when it fails in a CI log at 2am; test_total_ignores_removed_items tells them exactly what broke without opening the file. The same discipline applies to keeping one assertion's worth of behaviour per test where practical — a test that checks five unrelated things in a row will fail on the first one and give no signal about the other four until that first bug is fixed and the test is re-run, which slows down debugging far more than the extra few lines of a second test method would have cost.