Python ships with a test framework built into the standard library: unittest. You don't install anything, you don't add a dependency, you just import unittest and start writing test classes. This chapter covers the framework's mechanics: how to subclass TestCase, how the assertion methods work, what setUp and tearDown do, how to run the tests, and how to skip the ones that don't apply. Knowing unittest matters because the standard library uses it, many older codebases are built on it, and the lifecycle ideas show up in every test framework you'll ever touch.
unittestThree situations make unittest the right tool. The first is that it's already there. If you're working in an environment where installing third-party packages is a hassle (a locked-down CI runner, an embedded interpreter, a constrained Docker image), unittest works out of the box.
The second is that the Python standard library itself uses unittest for its own tests, so reading CPython's test suite, or any project that grew up alongside it, means reading unittest code. Being able to follow TestCase subclasses is a small but real skill.
The third is interoperability. pytest can run unittest-style tests without changes, which means a project can start with unittest and migrate piece by piece. Going the other way doesn't work as cleanly, so understanding the baseline is useful even if you eventually move on.
Where unittest falls short is in its style. Every test goes inside a class. Every assertion is a method call. Setup and teardown are class methods with fixed names. Compared to pytest's plain functions and bare assert statements, unittest feels heavier. The framework's mechanics are worth understanding on their own.
TestCaseA test class is any class that inherits from unittest.TestCase. Each test is a method on that class whose name starts with test_. The framework scans for that prefix and treats every matching method as a separate test. Methods that don't start with test_ are ignored by the runner, which is how you write helper methods on the same class without confusing the discovery process.
Here's a minimal class that tests a small Cart object:
Three dots, one per passing test. The line unittest.main() at the bottom is what kicks off the runner when you execute the file directly. It scans the current module for TestCase subclasses, collects every test_-prefixed method, and runs them.
Each test method is its own world. The framework creates a fresh instance of the test class for every test method, so anything you set on self in one test doesn't leak into the next. That isolation is the most important property of any test framework: a test's outcome depends only on the code under test and what the test itself sets up, not on the order tests happen to run in.
Method names matter. Renaming test_empty_cart_total_is_zero to check_empty_cart would make the framework skip it silently, because the test_ prefix is the only thing telling unittest "this is a test". The convention is test_<what_it_checks>, and the longer and more descriptive the better. The name is what shows up in failure reports, so future-you (or a teammate at 2am) reading "test_two_items_total failed" learns more than "test_5 failed".
unittest doesn't use the plain assert statement. Instead, TestCase provides a long list of assertion methods, each one tailored to a specific comparison. They all do the same fundamental thing (raise an AssertionError if the check fails), but the dedicated methods produce better failure messages than a bare assert would. When assertEqual(a, b) fails, you see both values; when assert a == b fails with unittest, you'd see "AssertionError" and nothing useful.
Here's the full set you'll actually reach for:
| Method | What it checks | Example |
|---|---|---|
assertEqual(a, b) | a == b | self.assertEqual(cart.total(), 39.98) |
assertNotEqual(a, b) | a != b | self.assertNotEqual(cart.total(), 0) |
assertTrue(x) | bool(x) is True | self.assertTrue(cart.has_discount) |
assertFalse(x) | bool(x) is False | self.assertFalse(cart.is_empty()) |
assertIs(a, b) | a is b | self.assertIs(found, original) |
assertIsNot(a, b) | a is not b | self.assertIsNot(copy, original) |
assertIsNone(x) | x is None | self.assertIsNone(cart.coupon) |
assertIsNotNone(x) | x is not None | self.assertIsNotNone(order.id) |
assertIn(item, container) | item in container | self.assertIn("Mouse", names) |
assertNotIn(item, container) | item not in container | self.assertNotIn("Keyboard", names) |
assertIsInstance(obj, cls) | isinstance(obj, cls) | self.assertIsInstance(cart.total(), float) |
assertNotIsInstance(obj, cls) | not isinstance(obj, cls) | self.assertNotIsInstance(cart, list) |
assertAlmostEqual(a, b) | rounds to 7 decimal places | self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertGreater(a, b) | a > b | self.assertGreater(cart.total(), 0) |
assertGreaterEqual(a, b) | a >= b | self.assertGreaterEqual(stock, 1) |
assertLess(a, b) | a < b | self.assertLess(discount, 1.0) |
assertLessEqual(a, b) | a <= b | self.assertLessEqual(weight, max_weight) |
assertRaises(exc, fn, *args) | calling fn(*args) raises exc | self.assertRaises(KeyError, dict.pop, {}, "k") |
assertRaisesRegex(exc, regex, fn, *args) | raises exc with message matching | self.assertRaisesRegex(ValueError, r"negative", ...) |
assertCountEqual(a, b) | same elements, order-independent | self.assertCountEqual([1, 2, 2], [2, 1, 2]) |
assertListEqual(a, b) | list-specific equality + diff | self.assertListEqual(got, expected) |
assertDictEqual(a, b) | dict-specific equality + diff | self.assertDictEqual(got, expected) |
assertSetEqual(a, b) | set-specific equality + diff | self.assertSetEqual(got, expected) |
The vast majority of tests you'll write use assertEqual, assertTrue, assertIn, assertIsNone, and assertRaises. The rest exist because they produce specialized failure messages. assertListEqual knows how to print a side-by-side diff of two lists. assertAlmostEqual knows to compare floats with a tolerance. assertIs distinguishes "same object" from "equal value", which matters for sentinel values like None or True.
A small worked example showing the most useful five:
Notice the last test. assertRaises is special: it can be used as a context manager (the recommended form) or as a function call (self.assertRaises(ValueError, Order, [])). The context manager form reads naturally: "this block of code should raise ValueError". If the block runs without raising, the test fails. If it raises a different exception, the test errors out (which is treated as a failure but reported differently).
assertRaises also exposes the caught exception, which is useful when you want to check the message:
cm.exception holds the actual ValueError instance after the block. You can inspect its message, its arguments, anything you'd inspect in an except clause. assertRaisesRegex skips the manual assertIn step by letting you check the message against a regular expression in one call.
A small but important note about assertEqual on floats. Floating-point arithmetic is approximate, so direct equality comparisons trip you up:
0.1 + 0.2 is not exactly 0.3 in binary floating point; it's 0.30000000000000004. assertEqual checks exact equality and fails. assertAlmostEqual rounds both numbers to 7 decimal places by default and compares those, which is what you want for any price math involving fractional amounts. You can pass places= for more or fewer digits, or delta= for a custom tolerance.
Cost: assertAlmostEqual defaults to comparing the rounded difference. For very small numbers (below 1e-7), the default tolerance is too coarse and may pass tests that shouldn't. Use delta= explicitly when comparing values near zero or at any unusual scale.
The collection-specific assertions (assertListEqual, assertDictEqual, assertSetEqual) are worth using when you care about a clear failure message. assertEqual([1, 2, 3], [1, 2, 4]) works fine, but the failure message is a wall of text. assertListEqual produces a diff:
The "First differing element 3" line pinpoints where the lists diverged. For longer or nested lists, that's the difference between "spend 20 seconds debugging" and "spend 5 minutes scrolling".
assertCountEqual is the one most people forget. It checks that two sequences contain the same elements, ignoring order. The name is misleading; it doesn't count anything. It's useful when you have a function that returns items in an unpredictable order (a set-backed result, a dict iteration before guaranteed ordering, a parallel result) and you only care that the contents match.
The function returned ["Wireless Mouse", "Keyboard"]. The test expected ["Keyboard", "Wireless Mouse"]. assertCountEqual doesn't care about order; both lists contain the same elements, so the test passes.
setUp and tearDown: Per-Test HooksMost test classes contain several tests that all need similar setup. Three tests of a Cart class might all start with "create a cart and add a few items". Repeating that boilerplate in every test method gets old fast, and copy-pasted setup tends to drift apart over time.
TestCase provides two hooks for this: setUp runs before every test method, tearDown runs after every test method. Both are instance methods, both take only self, and the framework calls them automatically. You don't invoke them from your test code; you just override them in your subclass.
setUp fired three times, once before each test. tearDown fired three times, once after each. Even though test_remove_decreases_count removed an item from the cart, the next test got a fresh cart with both items, because setUp created a new one. This is the isolation property again: every test starts from the same known state.
tearDown is the place for cleanup that has to happen no matter what. Closing files, deleting temp directories, restoring environment variables, resetting global state. It runs even if the test method raised an exception, which is the key reason you'd put cleanup there instead of at the end of the test method itself.
Each test gets a fresh temp file from setUp and that file is deleted in tearDown. If the test method raised partway through, tearDown would still run and the file would still be deleted. That guarantee is the whole reason for the hook.
Cost: setUp runs before every test method. If your setup is expensive (opening a database connection, building a large fixture, doing network I/O), you pay that cost N times for N tests. setUpClass, covered next, runs once per class instead. Use it when the setup can be safely shared.
setUpClass and tearDownClass: Per-Class HooksSome setup is too expensive to repeat for every test. Opening a database connection, building a large in-memory dataset, starting a subprocess. For those cases, setUpClass and tearDownClass run once per class: once before any test method runs, once after all of them finish.
The catch is that these hooks are @classmethods, not instance methods. They receive cls (the class itself), not self. You store the setup result as a class attribute. Every test then reads the same shared object.
setUpClass ran once, before any test. Four tests ran, each accessing self.catalog (which works because instance attribute lookup falls back to class attributes). tearDownClass ran once, after the last test.
The trade-off is sharing. Because every test sees the same catalog object, a test that mutates it can leak state into the next test. The four tests above only read from the catalog, so they're safe. If test_find_existing_product had called self.catalog.products.append(...), the appended item would be visible to every later test. That's the whole reason setUp (per-test) exists as a separate hook: when state can be mutated, you want a fresh instance for each test.
The diagram below shows the full lifecycle Python walks through when running a test class:
The outer hooks (setUpClass, tearDownClass) fire once. The inner hooks (setUp, tearDown) fire once per test. If a test class has 50 tests, setUpClass runs once and setUp runs 50 times. The asymmetry is what makes the per-class hooks worthwhile for expensive setup, and what makes the per-test hooks essential when isolation matters.
tearDownClass runs even if a test failed, but it does not run if setUpClass itself raised. If your setUpClass allocates two resources and the second allocation fails, the first one leaks unless setUpClass catches its own error and cleans up before re-raising. This mirrors the same rule from the context manager chapter: setup methods own their own partial-failure cleanup.
setUpModule and tearDownModuleOne level above the class are module-level hooks. setUpModule and tearDownModule are plain functions (not methods) defined at the top level of the test file. They run once for the whole module, before any class in the module is touched, and once after every class is done.
Use module-level hooks for setup that genuinely spans multiple classes in the same file: loading a large fixture file, starting a test database, mocking an environment variable. In practice they're rare; most projects keep their setup at the class level because that's where the scoping is most predictable. If you find yourself reaching for module-level hooks, it's worth asking whether the classes should share a base class with setUpClass instead.
The four-level hierarchy (module, class, instance, method) is summarized below:
| Hook | Scope | Type | Called |
|---|---|---|---|
setUpModule | module | function | once before any class in the module |
setUpClass | class | classmethod | once before any test in the class |
setUp | instance | method | before every test method |
| test method | one test | method | itself |
tearDown | instance | method | after every test method |
tearDownClass | class | classmethod | once after all tests in the class |
tearDownModule | module | function | once after all classes in the module |
The pattern is symmetric: setup goes outside-in (module, then class, then instance), teardown goes inside-out (instance, then class, then module). It's the same structure as nested with statements from the context manager chapter.
There are three ways to run unittest tests from the command line, and you'll use all three at different times.
The first runs a single file directly, assuming the file ends with unittest.main():
This works for ad-hoc development. You run the file, the unittest.main() call picks up the TestCase classes in the module, and the results print. Add -v for verbose output:
The verbose flag prints one line per test name plus the result, instead of one dot per pass. Useful when something's going wrong and you want to see which test even ran.
The second invokes unittest as a module against a specific file or test:
The dotted form lets you target a module, a class, or a single test method. This is what you reach for when one test is failing and you want to iterate on it without running the rest. Single-test runs are a few hundred milliseconds; full suites can be minutes.
The third is test discovery, which scans a directory tree for test files automatically:
discover without arguments looks in the current directory for files matching test*.py. The -s flag (start directory) and -p flag (pattern) override the defaults. Run from your project root, this finds every test file in the tree, every TestCase subclass in those files, every test_-prefixed method in those classes, and runs them all.
Discovery has one rule that surprises people: it walks into subdirectories only if they contain an __init__.py file (so they're proper packages). A tests/ folder without an __init__.py won't be descended into. The fix is to add an empty __init__.py to every test folder, or pass --top-level-directory explicitly. Modern projects on Python 3.3+ get implicit namespace packages, so an empty folder works too, but the explicit __init__.py is safer.
Cost: Discovery imports every matching file. If your test files have heavy module-level imports (loading a large dataset, opening a connection, starting a server), every discovery run pays that cost up front. Push expensive setup into setUpClass or setUpModule so it only runs when tests in that file actually execute.
A failure looks like this:
The dots show pass/fail status per test. . is a pass, F is a failure (an assertion didn't hold), E is an error (an unexpected exception, not from an assertion), s is a skip, x is an expected failure, u is an unexpected pass (a test marked expectedFailure that surprisingly passed). The summary at the end shows totals. Anything other than "OK" at the end means at least one test didn't pass cleanly.
Some tests shouldn't run in every environment. A test that hits the network shouldn't run when offline. A test that uses a Linux-only feature shouldn't run on Windows. A test for a feature you haven't built yet shouldn't fail and pollute the results; it should announce "not implemented" cleanly.
unittest provides decorators for each case.
@unittest.skip(reason) skips the test unconditionally. Use it when a test is broken and you're tracking the fix in another commit:
@unittest.skipIf(condition, reason) skips when the condition is truthy:
@unittest.skipUnless(condition, reason) is the inverse: skip unless the condition is truthy. It reads naturally for "only run when":
@unittest.expectedFailure is the odd one. It marks a test that's currently broken in a known way. If the test fails, the framework reports it as "expected failure" and the overall run still passes. If the test unexpectedly passes, the framework reports it as "unexpected success" and the run fails. The point is to track known bugs without breaking the build, and to find out immediately when the bug gets fixed (so you can remove the decorator).
The first test failed (as expected). The second test passed (unexpectedly), which fails the run. The signal is "you marked this test as broken, but it works now; go remove the decorator".
You can also skip from inside a test by calling self.skipTest(reason):
self.skipTest is useful when the skip condition depends on state that's only known at runtime (a config flag, a database query, an environment probe). The decorator form is preferable when the condition is known statically.
subTest: Many Assertions in One TestSometimes you want to test the same logic against several inputs without writing one method per case. unittest provides self.subTest() as a context manager: each block inside is reported separately if it fails, but the whole thing counts as one test method.
If one case fails, you still see which one. Say the discount function had a bug for percent=50:
The subTest(price=..., percent=...) parameters are printed in the failure header, so you know exactly which row of the table broke. Without subTest, the first failed assertion would abort the loop and you'd never know about cases 4 and 5.
subTest is the closest unittest gets to "parametrized" tests. It works, but it's clumsy compared to pytest.mark.parametrize. The pytest version generates one test ID per row in the test report, runs each row as a separate test, and integrates with all the other pytest tools (filtering by ID, re-running just one row, and so on). If you're starting a new project and you need parametrization, pytest's approach is worth the switch on its own.
For an existing unittest codebase, subTest is fine. Just remember that within a single subTest block, an assertion failure does abort the rest of that block (not the whole test); execution then moves to the next iteration of the loop.
10 quizzes