Unit Testing (unittest)
As a program grows past a handful of functions, it becomes impossible to manually re-check every code path each time you make a change. Automated tests solve this by encoding your expectations about how the code should behave into small, repeatable pieces of code themselves. Run them once, and you instantly know whether a change broke something — without clicking through the application by hand every single time.
Why Automated Testing Matters
Catching regressions — a test written today keeps protecting you months from now, when someone (possibly you) changes unrelated code and accidentally breaks this behavior
Documenting expected behavior — a well-named test is executable documentation; reading the test suite tells a new contributor exactly what the code is supposed to do, including edge cases
Enabling confident refactoring — with a solid test suite, you can restructure internals freely and know immediately if you broke observable behavior, instead of guessing
Python ships a full-featured testing framework in its standard library, so you don't need any third-party dependency to get started: the unittest module.
Writing a TestCase
Tests in unittest are organized as methods on a class that inherits from unittest.TestCase. Each method whose name starts with test is treated as an individual, independently runnable test.
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b# test_calculator.py
import unittest
from calculator import add, divide
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_by_zero_raises(self):
with self.assertRaises(ValueError):
divide(1, 0)
if __name__ == "__main__":
unittest.main()Each test method should check one specific behavior and be independent of the others — a test shouldn't rely on another test having run first, since unittest doesn't guarantee execution order.
Common Assertion Methods
TestCase provides a family of assert* methods that both check a condition and produce a readable failure message when it isn't met — far more informative than a bare assert statement.
Method | Checks |
|---|---|
assertEqual(a, b) | That a and b are equal (a == b) |
assertNotEqual(a, b) | That a and b are not equal (a != b) |
assertTrue(x) | That x is truthy (bool(x) is True) |
assertFalse(x) | That x is falsy (bool(x) is False) |
assertRaises(Error) | That the code inside the with block raises the given exception type |
assertIn(item, container) | That item is present inside container |
assertIsNone(x) | That x is exactly None |
Running Your Tests
You can run a single test file directly, or let unittest discover every test file in a project automatically.
# Run one file python -m unittest test_calculator.py # Auto-discover and run every test_*.py file in the project python -m unittest discover # Run with more detailed output python -m unittest -v test_calculator.py
.... ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
Each dot represents one passing test. If a test fails, unittest replaces the dot with an F (for a failed assertion) or an E (for an unexpected exception) and prints a full traceback pointing at exactly which assertion failed and what values it saw.
Test Fixtures: setUp and tearDown
Many tests need the same preparation before they run — creating a temporary object, opening a connection, resetting some state — and the same cleanup afterward. Repeating that setup code in every single test method is tedious and easy to get out of sync. TestCase solves this with two special methods: setUp, which runs before every test method in the class, and tearDown, which runs after every test method, even if the test failed.
import unittest
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def total_items(self):
return len(self.items)
class TestShoppingCart(unittest.TestCase):
def setUp(self):
# Runs before every single test method below
self.cart = ShoppingCart()
self.cart.add("apple")
def tearDown(self):
# Runs after every single test method, pass or fail
self.cart = None
def test_starts_with_one_item(self):
self.assertEqual(self.cart.total_items(), 1)
def test_add_increases_count(self):
self.cart.add("banana")
self.assertEqual(self.cart.total_items(), 2)
self.assertIn("banana", self.cart.items)
if __name__ == "__main__":
unittest.main()Because setUp runs fresh before each test, test_starts_with_one_item and test_add_increases_count each get their own brand-new ShoppingCart with exactly one apple in it — neither test can accidentally leak state into the other, which is exactly the isolation you want between independent tests.
Summary
Automated tests catch regressions, document expected behavior, and make refactoring safe
unittest is Python's built-in testing framework — subclass TestCase and write methods starting with test
assertEqual, assertTrue, assertRaises, assertIn, and similar methods check conditions and produce readable failure output
Run tests with python -m unittest, or python -m unittest discover to auto-find every test file in a project
setUp and tearDown run before/after every test method, giving each test a clean, isolated starting state