PythonUnit Testing (unittest)

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.

Python
# 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

Python
# 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

Tip
Prefer the specific assertion over a generic one wherever possible. assertEqual(a, b) produces a failure message showing both values when it fails; assertTrue(a == b) only tells you the expression was False, without showing what a and b actually were. The more specific method, the more useful the failure output.
Running Your Tests

You can run a single test file directly, or let unittest discover every test file in a project automatically.

Bash
# 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.

Python
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.

Note
For setup or teardown that should run once for an entire test class rather than before/after every individual method — like opening one expensive database connection shared across many tests — unittest also provides classmethods setUpClass and tearDownClass. Use setUp/tearDown for per-test isolation, and the Class variants only when re-doing the setup for every test would be wasteful and the resource is safe to share.
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