unittest
Python's standard-library unittest module provides a full xUnit-style testing framework — TestCase classes, rich assertion methods, setup/teardown hooks, and a built-in test runner, no third-party install required.
unittest is Python's built-in testing framework, included in the standard library, modeled on the xUnit family of testing tools (originally Java's JUnit). It organizes tests as methods on TestCase subclasses, provides dozens of specific assertion methods with informative failure messages, supports setup/teardown hooks for test isolation, and ships with a command-line test runner. This topic covers unittest's concrete API; for the underlying testing concepts (unit vs integration tests, isolation, what makes a good assertion) see the Testing Fundamentals topic.
TestCase: The Core Building Block
Tests in unittest are written as methods on a class that subclasses unittest.TestCase. Any method whose name starts with test is automatically discovered and run as a test; other methods (helpers, setup code) are ignored by the runner. Each test method should be independent and self-contained.
unittest.main() discovers every test* method on every TestCase subclass in the module and runs them, reporting pass/fail for each.
import unittest
def add(a: int, b: int) -> int:
return a + b
class TestAdd(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)
if __name__ == "__main__":
unittest.main()Assertion Methods
TestCase provides dozens of assert* methods that are more expressive than a bare assert statement — each produces a clear, specific failure message showing both the actual and expected values, and several encode a specific kind of check (equality, identity, containment, exception-raising, floating-point closeness) that a plain assert would need extra code to express as clearly.
assertAlmostEqual handles floating-point comparisons where exact equality would be unreliable; assertRaises used as a context manager checks that the wrapped block raises the expected exception type.
Prefer the specific assertion (assertEqual, assertIn, assertIsInstance) over a generic assertTrue(x == y) — the specific form gives a much more informative failure message (showing both actual and expected values) when the test fails.
import unittest
class TestAssertions(unittest.TestCase):
def test_examples(self):
self.assertEqual(2 + 2, 4)
self.assertNotEqual(2 + 2, 5)
self.assertTrue(3 > 1)
self.assertFalse(1 > 3)
self.assertIsNone(None)
self.assertIsNotNone(42)
self.assertIn(3, [1, 2, 3])
self.assertIsInstance("hi", str)
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
with self.assertRaises(ZeroDivisionError):
1 / 0assertRaises for Exception Testing
assertRaises verifies that a specific exception type is raised, either by passing a callable and its arguments directly, or (more commonly, and more flexibly) by using it as a context manager around a with block. The context-manager form also exposes the caught exception via as, letting you additionally assert on its message or attributes.
import unittest
def validate_age(age: int) -> None:
if age < 0:
raise ValueError(f"age cannot be negative: {age}")
class TestValidateAge(unittest.TestCase):
def test_negative_age_raises(self):
with self.assertRaises(ValueError) as ctx:
validate_age(-5)
self.assertIn("negative", str(ctx.exception))setUp and tearDown: Per-Test Fixtures
setUp(self) runs before EVERY test method in the class, and tearDown(self) runs after every test method (even if the test failed or raised) — the standard place to create fresh fixtures (objects, temp files, database connections) and clean them up, keeping each test isolated from the others (see Test Isolation in the Testing Fundamentals topic).
import unittest
class ShoppingCart:
def __init__(self):
self.items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart() # fresh cart before every test method
def test_starts_empty(self):
self.assertEqual(self.cart.items, [])
def test_add_item(self):
self.cart.add("apple")
self.assertEqual(self.cart.items, ["apple"])
# this test never sees leftover items from test_starts_empty --
# setUp gave it a brand-new ShoppingCartsetUpClass and tearDownClass: Class-Level Fixtures
setUpClass(cls) and tearDownClass(cls) (both @classmethods) run ONCE for the whole TestCase class — before any test in it, and after all tests in it — rather than once per test method. They're for genuinely expensive, safely-shareable setup (e.g. starting a test database server) where per-test recreation would be wastefully slow; anything mutated by individual tests still belongs in setUp/tearDown to preserve isolation.
import unittest
class TestWithExpensiveResource(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.expensive_resource = "pretend this took 5 seconds to start"
@classmethod
def tearDownClass(cls):
cls.expensive_resource = None
def test_uses_resource(self):
self.assertIsNotNone(self.expensive_resource)Skipping Tests
unittest provides decorators to mark tests as intentionally skipped rather than failing: @unittest.skip(reason) always skips, @unittest.skipIf(condition, reason) skips conditionally, and @unittest.skipUnless(condition, reason) runs only when a condition holds (useful for platform-specific or environment-dependent tests). Skipped tests are reported separately from passes and failures, so they remain visible rather than silently vanishing.
import sys
import unittest
class TestPlatformSpecific(unittest.TestCase):
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_linux_only_feature(self):
self.assertTrue(True)
@unittest.skip("not implemented yet")
def test_future_feature(self):
self.fail("should never run")Subtests: subTest for Data-Driven Cases
self.subTest(...) (used as a context manager inside a loop) lets a single test method check multiple related cases while reporting each one's pass/fail independently, instead of stopping at the first failure. This is especially useful when looping over a table of inputs/expected-outputs: without subTest, the first failing case would abort the whole test method and hide any later failures in the same loop.
subTest(value=value) labels each iteration so the test report identifies exactly which input failed, and lets remaining iterations still run and report their own results.
import unittest
def square(n: int) -> int:
return n * n
class TestSquare(unittest.TestCase):
def test_square_many_values(self):
cases = [(2, 4), (3, 9), (4, 15)] # deliberate bug: (4, 15) should be 16
for value, expected in cases:
with self.subTest(value=value):
self.assertEqual(square(value), expected)
# without subTest, only the FIRST failure would be reported and the loop would stop;
# with subTest, unittest reports each failing case (here, just value=4) separatelyMocking Overview: unittest.mock
unittest.mock (standard library, bundled with unittest) provides Mock and MagicMock objects that stand in for real dependencies, recording how they were called so a test can assert on the interaction, plus patch() to temporarily replace an object/function in a module's namespace for the duration of a test. This is a large enough topic to deserve its own deep coverage elsewhere — the essential idea here is that mocking lets unit tests isolate the code under test from slow, unpredictable, or hard-to-trigger real dependencies (network calls, the current time, external services).
import unittest
from unittest.mock import Mock, patch
def send_notification(mailer, address: str) -> None:
mailer.send(address, "Hello!")
class TestNotification(unittest.TestCase):
def test_send_notification_calls_mailer(self):
mock_mailer = Mock()
send_notification(mock_mailer, "[email protected]")
mock_mailer.send.assert_called_once_with("[email protected]", "Hello!")
@patch("time.time", return_value=1_700_000_000.0)
def test_something_using_current_time(self, mock_time):
import time
self.assertEqual(time.time(), 1_700_000_000.0)Running Tests: unittest.main() and Discovery
unittest.main() at the bottom of a test file lets you run that file directly (python test_module.py) to execute its tests. For a whole project, python -m unittest discover walks a directory tree finding files matching a pattern (test*.py by default) and runs every test in them — no need to import each test module manually. Test method/class/file naming conventions (test_ prefixes, Test class prefixes) exist specifically so discovery can find tests automatically.
$ python -m unittest discover -s tests -p "test_*.py"
...F..
======================================================================
FAIL: test_add_negative_numbers (tests.test_math.TestAdd)
----------------------------------------------------------------------
AssertionError: -3 != -2
----------------------------------------------------------------------
Ran 6 tests in 0.003s
FAILED (failures=1)Practical Usage
unittest is a solid, dependency-free choice for any project that wants to avoid a third-party install, and its xUnit-style structure (explicit TestCase classes, setUp/tearDown) is familiar to developers coming from Java, C#, or similar ecosystems. The third-party pytest framework (much more widely used in the broader Python ecosystem) offers a lighter-weight, function-based style with plain assert statements and powerful fixture injection, and can even discover and run unittest-style tests — but unittest remains fully capable on its own and requires nothing beyond the standard library.
Best Practices
Name test methods descriptively enough that a failure's name alone hints at what broke (test_negative_age_raises_value_error, not test_1). Use setUp for fixtures needed by most/all tests in a class, but keep it fast — expensive one-time setup belongs in setUpClass. Prefer the most specific assertion method available (assertIn over assertTrue(x in y)) for clearer failure output. Use subTest when looping over data-driven cases so every failing case is visible, not just the first. Keep mocking focused on genuinely external/slow dependencies — mocking your own simple, fast, pure functions usually adds noise without benefit.
Key Takeaways
Tests are methods (named test*) on classes subclassing unittest.TestCase; unittest.main() or python -m unittest discover finds and runs them.
Specific assertion methods (assertEqual, assertIn, assertRaises, assertAlmostEqual, etc.) give clearer failure messages than a bare assert.
setUp/tearDown run before/after every test method for per-test fixtures; setUpClass/tearDownClass run once per class for expensive shared setup.
assertRaises (often as a context manager) verifies that specific code raises a specific exception type, optionally inspecting the exception itself.
subTest lets a data-driven test method report each case's pass/fail independently instead of stopping at the first failure.
@skip/skipIf/skipUnless mark tests as intentionally not run, keeping them visible in reports rather than silently deleted.
unittest.mock provides Mock/MagicMock and patch() for isolating tests from slow or unpredictable real dependencies.
class TestMath(unittest.TestCase): def check_addition(self): # doesn't start with 'test' -- never discovered or run! self.assertEqual(1 + 1, 2) class TestMath(unittest.TestCase): def test_addition(self): # 'test' prefix -- discovered and run automatically self.assertEqual(1 + 1, 2) unittest's default test discovery only picks up methods whose names start with 'test'; a method named anything else silently never runs, giving a false sense that it's covered.
class TestCart(unittest.TestCase): cart = ShoppingCart() # created once at class definition time -- shared across ALL tests def test_add_item(self): self.cart.add("apple") self.assertEqual(len(self.cart.items), 1) # fails if another test already added items class TestCart(unittest.TestCase): def setUp(self): self.cart = ShoppingCart() # fresh instance before every test method def test_add_item(self): self.cart.add("apple") self.assertEqual(len(self.cart.items), 1)
A class attribute is created once and shared by every test method in the class, breaking isolation; use setUp to give each test its own fresh fixture instance.
def test_squares(self): for value, expected in [(2, 4), (3, 9), (4, 15)]: self.assertEqual(value * value, expected) # stops at the FIRST failing case (4, 15) def test_squares(self): for value, expected in [(2, 4), (3, 9), (4, 15)]: with self.subTest(value=value): self.assertEqual(value * value, expected) # reports every failing case
Without subTest, a loop of assertions aborts the whole test method at the first failure, hiding any later cases that would also have failed; subTest reports each case's own pass/fail.
What naming convention must a method follow for unittest's default test discovery to find and run it?
Its name must start with 'test' (e.g. test_addition); methods with any other name are treated as ordinary helper methods and are never automatically run as tests.
What is the difference in when setUp/tearDown run versus setUpClass/tearDownClass?
setUp and tearDown run before and after EVERY individual test method in the class. setUpClass and tearDownClass (classmethods) run only ONCE for the whole class -- before any test runs, and after all tests in the class have finished.
Why is self.assertIn(x, y) generally preferred over self.assertTrue(x in y)?
The specific assertion method (assertIn) produces a clearer, more informative failure message showing both the searched-for value and the container's contents; assertTrue on a boolean expression only reports that the condition was falsy, without that detail.
What problem does self.subTest() solve when looping over multiple test cases in one test method?
Without subTest, the loop aborts at the FIRST assertion failure, hiding any other cases in the same loop that would also fail. Wrapping each iteration in with self.subTest(...): lets unittest report every failing case independently while still running the rest of the loop.
How do you verify that a specific piece of code raises a specific exception type using unittest?
Use self.assertRaises(ExceptionType) as a context manager around the code: with self.assertRaises(ValueError): risky_call(). It also supports as ctx to capture and further inspect the raised exception, e.g. self.assertIn('message text', str(ctx.exception)).
What is unittest.mock used for, and what specific problem does it solve for unit tests?
It provides Mock/MagicMock objects (recording how they were called, for later assertions) and patch() (temporarily replacing an object/function during a test), letting unit tests isolate the code under test from slow, unpredictable, or hard-to-trigger real dependencies like network calls or the system clock.
What command runs every test across a whole project directory without importing each test module manually?
python -m unittest discover (optionally with -s to set the start directory and -p to set the filename pattern, default test*.py) walks the directory tree, finds matching test files, and runs every discovered test in them.
Write a TestCase class TestStack for a simple Stack class (with push, pop, and is_empty methods) using setUp for a fresh stack per test, covering: pushing adds an item, popping returns the last pushed item, and popping an empty stack raises IndexError.
import unittest class Stack: def __init__(self): self._items: list = [] def push(self, item) -> None: self._items.append(item) def pop(self): if not self._items: raise IndexError("pop from empty stack") return self._items.pop() def is_empty(self) -> bool: return len(self._items) == 0 class TestStack(unittest.TestCase): def setUp(self): self.stack = Stack() def test_new_stack_is_empty(self): self.assertTrue(self.stack.is_empty()) def test_push_adds_item(self): self.stack.push(1) self.assertFalse(self.stack.is_empty()) def test_pop_returns_last_pushed(self): self.stack.push(1) self.stack.push(2) self.assertEqual(self.stack.pop(), 2) def test_pop_empty_raises_index_error(self): with self.assertRaises(IndexError): self.stack.pop() if __name__ == "__main__": unittest.main()
setUp gives every test method its own fresh Stack, so tests are isolated from each other; each test method checks exactly one behavior, and assertRaises verifies the empty-pop error case using the context-manager form.
In what order do 'setUp', 'test_one'/'test_two', and 'tearDown' print (assuming both tests run)?
setUp, then one of the test methods, then tearDown -- repeated once per test method: setUp / test_one / tearDown / setUp / test_two / tearDown (exact order of test_one vs test_two can vary, but each is always sandwiched between its own setUp and tearDown call, since both run once per individual test method, not once for the whole class).
Write a test using unittest.mock.patch to verify that a function get_greeting() (which calls datetime.date.today()) returns 'Good morning' when the mocked date's weekday is a weekday value it checks against, without depending on the real current date.
import unittest from unittest.mock import patch import datetime def get_greeting() -> str: today = datetime.date.today() return "Good morning" if today.weekday() < 5 else "Happy weekend" class TestGreeting(unittest.TestCase): @patch("datetime.date") def test_greeting_on_weekday(self, mock_date): mock_date.today.return_value = datetime.date(2026, 9, 8) # a Tuesday self.assertEqual(get_greeting(), "Good morning") if __name__ == "__main__": unittest.main()
@patch("datetime.date") replaces datetime.date with a Mock for the duration of the test, and setting mock_date.today.return_value fixes what datetime.date.today() returns, making the test deterministic and independent of the real current date -- exactly the kind of unpredictable dependency mocking is meant to isolate.
import unittest
class TestExample(unittest.TestCase):
def setUp(self):
print("setUp")
def test_one(self):
print("test_one")
def test_two(self):
print("test_two")
def tearDown(self):
print("tearDown")
unittest.main(argv=["ignored"], exit=False)