🐍 Python Course · Stage 3 · Lesson 39 of 89 · unittest — Testing
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← logging — Diagnostics functools — Function Tools →
thecodex.expert · The Codex Family of Knowledge
Python stdlib

Python unittest module

Automated tests that verify your code works — and keep working — as you change it.

Python 3.13 Standard Library Last verified:
Canonical Definition

The Python unittest module is the built-in testing framework — TestCase subclasses with assertion methods, setUp/tearDown lifecycle, unittest.mock for replacing dependencies, and a test runner for automated test discovery and execution.

🟩 Beginner

The built-in testing framework in depth

What you'll learn: Every assertion method in TestCase, the setUp/tearDown lifecycle, and how to run tests with discovery.

How to read this tab: Take the test class you wrote in the Testing lesson and add more assertion types. Use assertAlmostEqual on a float calculation. Use assertIn on a list. Use assertDictEqual to compare two dicts.

⏱ 30 min 📄 3 sections 🔶 Prerequisite: Testing lesson
One sentence

unittest is Python's built-in testing framework — it lets you write automated tests that verify your code does what you expect, and run them all with one command to catch regressions before they reach production.

💡

Naming tests is as important as writing them. test_add_positive_numbers is a good name. test_math is not. The name should describe the scenario being tested and the expected outcome. When a test fails, the name is what tells you (and future-you) what broke without reading the code.

Writing your first tests

Pythontest_basics.py
import unittest

# The code we're testing
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

# Tests live in a class that inherits from unittest.TestCase
class TestMathFunctions(unittest.TestCase):

    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_negative_numbers(self):
        self.assertEqual(add(-1, -2), -3)

    def test_add_zero(self):
        self.assertEqual(add(5, 0), 5)

    def test_divide_normal(self):
        self.assertAlmostEqual(divide(10, 3), 3.333, places=3)

    def test_divide_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

    def test_divide_by_zero_message(self):
        with self.assertRaises(ZeroDivisionError) as ctx:
            divide(10, 0)
        self.assertIn("Cannot divide", str(ctx.exception))

# Run tests from command line: python -m unittest test_basics.py
# Or discover all tests: python -m unittest discover
if __name__ == "__main__":
    unittest.main()

Assertion method selection matters for failure messages. If you use assertTrue(a == b) instead of assertEqual(a, b), a failure message says "False is not True" — useless. assertEqual says "Expected 5, got 7" — immediately actionable. Always use the most specific assertion: assertEqual, assertIn, assertIsNone, assertRaises, assertAlmostEqual (for floats).

Key assertion methods

MethodTests that
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 (identity)
assertIsNone(x)x is None
assertIn(a, b)a in b
assertIsInstance(a, T)isinstance(a, T)
assertRaises(Exc)block raises Exc
assertAlmostEqual(a, b)round(a-b, 7) == 0
assertGreater(a, b)a > b
assertRegex(s, r)re.search(r, s)

setUp/tearDown run around EVERY test — not once. If you have 10 test methods, setUp runs 10 times. This is intentional: each test gets a fresh, isolated state. For expensive one-time setup (like creating a database), use setUpClass(cls) and tearDownClass(cls) as class methods decorated with @classmethod.

setUp and tearDown

Pythontest_setup.py
import unittest
import tempfile
import os

class TestWithSetup(unittest.TestCase):

    def setUp(self):
        # Runs BEFORE each test method
        self.temp_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.temp_dir, "test.txt")
        with open(self.test_file, "w") as f:
            f.write("test content")

    def tearDown(self):
        # Runs AFTER each test method — even if test fails
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))

    def test_file_content(self):
        with open(self.test_file) as f:
            content = f.read()
        self.assertEqual(content, "test content")

    @classmethod
    def setUpClass(cls):
        # Runs once BEFORE all tests in this class
        cls.db = connect_to_test_database()

    @classmethod
    def tearDownClass(cls):
        # Runs once AFTER all tests in this class
        cls.db.close()

✅ Beginner tab complete — check your understanding

  • I know at least 8 assertion methods beyond assertEqual
  • I know setUp runs before EACH test method, not once before the class
  • I can run tests with python -m unittest discover to find all test files automatically

Stage 3 complete. Continue to Stage 4: Concepts →

🔵 Intermediate

unittest.mock — replacing dependencies in tests

What you'll learn: Mock and MagicMock objects for replacing real dependencies. The @patch decorator. call_args, assert_called_once_with(), and other mock inspection methods.

How to read this tab: Write a test that uses @patch to mock a function that makes a network request. Verify the mock was called with the right arguments.

⏱ 30 min 📄 1 section 🔶 Prerequisite: Beginner tab

The golden rule, repeated because it matters: patch where it is USED, not where it is defined. If your module imports with from requests import get, patch "yourmodule.get". If it imports with import requests, patch "requests.get". The target is the name that will be looked up when the code runs.

Mocking with unittest.mock

Tests should be isolated — they should not make real network requests, write to real databases, or depend on external services. unittest.mock lets you replace these dependencies with controllable fakes during tests.

Pythontest_mock.py
from unittest import TestCase
from unittest.mock import patch, MagicMock, call

def get_user_name(user_id):
    import requests
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()["name"]

class TestGetUserName(TestCase):

    @patch("requests.get")           # replace requests.get with a Mock
    def test_get_user_name(self, mock_get):
        # Configure what the mock returns
        mock_response = MagicMock()
        mock_response.json.return_value = {"name": "Priya", "id": 1}
        mock_get.return_value = mock_response

        result = get_user_name(1)

        # Assert return value
        self.assertEqual(result, "Priya")

        # Assert mock was called correctly
        mock_get.assert_called_once_with("https://api.example.com/users/1")

    @patch("requests.get", side_effect=ConnectionError("No internet"))
    def test_handles_network_error(self, mock_get):
        with self.assertRaises(ConnectionError):
            get_user_name(1)

    def test_mock_multiple_calls(self):
        mock = MagicMock()
        mock.side_effect = [10, 20, 30]   # returns these in order
        self.assertEqual(mock(), 10)
        self.assertEqual(mock(), 20)
        self.assertEqual(mock(), 30)
💡

subTest makes parametrised tests readable. Instead of looping with a single test method (where one failure stops the loop), use subTest: for x, expected in cases: with self.subTest(x=x): self.assertEqual(f(x), expected). All cases run even if some fail, and the failure message includes the subTest values.

Parametrised tests and subTest

Pythontest_parameterised.py
import unittest

def is_prime(n):
    if n < 2: return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0: return False
    return True

class TestIsPrime(unittest.TestCase):

    def test_primes(self):
        # subTest — each case shown separately on failure
        prime_cases = [2, 3, 5, 7, 11, 13, 17, 19, 23]
        for n in prime_cases:
            with self.subTest(n=n):
                self.assertTrue(is_prime(n), f"{n} should be prime")

    def test_non_primes(self):
        non_prime_cases = [0, 1, 4, 6, 8, 9, 10, 15, 25]
        for n in non_prime_cases:
            with self.subTest(n=n):
                self.assertFalse(is_prime(n), f"{n} should not be prime")

    @unittest.skip("not implemented yet")
    def test_large_primes(self):
        pass

    @unittest.skipIf(True, "always skipped")
    def test_conditional(self):
        pass
Commonly confused
Test method names must start with test. The test runner discovers methods by looking for names beginning with test. A method named check_something() will not run. This is a common source of tests that appear to pass but never actually ran.
assertEqual is not the same as assertTrue(a == b). assertEqual produces a better error message — it shows you the actual and expected values. assertTrue(a == b) just says "False is not True." Always use the most specific assertion method available.
unittest is not the only option — pytest is more popular in practice. pytest (third-party) has simpler syntax (no class required, plain assert), better output, fixtures, and parametrisation. But unittest is built-in and requires no installation — and pytest can run unittest-style tests.
How this connects

✅ Intermediate tab complete — check your understanding

  • I know the difference between Mock (basic) and MagicMock (also mocks dunder methods)
  • I can use @patch as a decorator to replace a dependency for the duration of a test
  • I can inspect mock.call_args_list to see every call that was made
  • I remember: patch where it is used, not where it is defined

Stage 3 complete. Continue to Stage 4: Concepts →

🔴 Expert

Test architecture, test doubles taxonomy, and TestSuite

What you'll learn: The formal taxonomy of test doubles (dummy, stub, spy, mock, fake). TestSuite for building custom test collections. The unittest architecture (TestCase → TestSuite → TestRunner → TestResult).

How to read this tab: Read when you want to communicate precisely about testing concepts with a team, or when customising the test runner.

⏱ 25 min 📄 2 sections 🔶 Prerequisite: After Stage 3
📎

The architecture enables customisation. TestCase holds tests. TestSuite aggregates them. TestRunner executes them. TestResult records outcomes. This separation means you can write a custom TestRunner that sends results to Slack, or a custom TestLoader that finds tests by a different naming convention.

unittest architecture: TestCase, TestSuite, TestRunner

The unittest framework has four core components. TestCase: the base class for individual test classes; defines assertions and lifecycle methods. TestSuite: a collection of test cases and suites — allows grouping and ordering tests. TestLoader: discovers and loads tests from modules, classes, or methods — unittest.TestLoader().loadTestsFromModule(module). TestRunner: executes the suite and reports results — the default is TextTestRunner; CI systems use custom runners (JUnit XML, HTML reports). The TestResult object accumulates results — successes, failures, errors, skips — and is passed to runners.

Test isolation and the test doubles taxonomy

Gerard Meszaros's xUnit Test Patterns (2007) defines the taxonomy: Dummy — passed but never used (placeholder). Stub — returns hardcoded values. Spy — records calls made to it. Mock — a spy with expectations; fails if not used as expected. Fake — a working implementation simplified for testing (e.g., in-memory database). Python's MagicMock is a spy/mock hybrid — it records calls and can assert on them, but does not automatically fail if assertions are not made (unlike Java Mockito).

✅ Expert tab complete

  • I know the difference between a stub (returns canned data), a mock (verifies calls), and a fake (working implementation)
  • I can build a custom TestSuite to run specific tests in a specific order

Stage 3 complete. Continue to Stage 4: Concepts →

Sources

1
Python Standard Library — unittest. docs.python.org/3/library/unittest.html.
2
Python Standard Library — unittest.mock. docs.python.org/3/library/unittest.mock.html.
3
Meszaros, G. (2007). xUnit Test Patterns. Addison-Wesley.
Source confidence: High Last verified: Primary source: docs.python.org/3/library/unittest.html

Sources

1
Python Standard Library — unittest. docs.python.org/3/library/unittest.html.
2
Python Standard Library — unittest.mock. docs.python.org/3/library/unittest.mock.html.
3
Meszaros, G. (2007). xUnit Test Patterns. Addison-Wesley.