Python `unittest` Explained

Learn the core features of Python's built-in unittest framework: assertion methods, setUp and tearDown, test discovery, skipping tests, and subtests.

8 min read

Python's unittest module is a full testing framework included in every Python installation. You do not need to install it, configure it, or choose between competing options. When you write import unittest, you have access to test cases, test suites, test runners, and a command-line interface that discovers and executes your tests.

This article builds on the test you wrote in write your first Python unit test and covers the features you will use every day: the assertion methods, the setUp and tearDown methods for preparing test data, command-line options, and the skip and subtest utilities.

The assertion methods you will use most

Every test ends with an assertion, a statement that declares what must be true for the test to pass. The unittest.TestCase class provides a family of assertion methods that are more informative than the built-in assert statement. When an assertion fails, these methods print both the expected value and the actual value.

The assertEqual method is the workhorse. It checks that two values are equal and works with strings, numbers, lists, dictionaries, and most built-in types.

pythonpython
import unittest
 
class TestAssertEqual(unittest.TestCase):
    def test_string_equality(self):
        self.assertEqual("hello", "hello")
 
    def test_list_equality(self):
        self.assertEqual([1, 2, 3], [1, 2, 3])

The assertTrue and assertFalse methods check boolean conditions. Use them when the function you are testing returns a boolean or when you need to verify a condition.

pythonpython
def test_value_in_range(self):
    age = 25
    self.assertTrue(18 <= age <= 65)
 
def test_list_is_not_empty(self):
    items = ["apple", "banana"]
    self.assertFalse(len(items) == 0)

The assertIn method checks membership in a container. It produces clearer failure messages than writing self.assertTrue(value in collection) because it prints both the value and the collection when the assertion fails.

pythonpython
def test_item_in_list(self):
    fruits = ["apple", "banana", "cherry"]
    self.assertIn("banana", fruits)

The assertRaises method verifies that a specific exception is raised. This matters when you test error handling: a function should raise a ValueError when given invalid input.

pythonpython
def test_divide_by_zero(self):
    with self.assertRaises(ZeroDivisionError):
        result = 10 / 0

The context manager form is the most readable pattern. The test passes only if the exact exception type is raised inside the block.

setUp and tearDown for shared test fixtures

Most test classes share setup code across multiple methods. Instead of creating the same data at the start of every method, define a setUp method that runs before each test. The framework calls setUp, then the test method, then tearDown, and repeats this for every test.

pythonpython
class TestShoppingCart(unittest.TestCase):
    def setUp(self):
        self.cart = []
        self.item = {"name": "book", "price": 12.99}
 
    def test_add_item(self):
        self.cart.append(self.item)
        self.assertEqual(len(self.cart), 1)
 
    def test_empty_cart_total(self):
        self.assertEqual(len(self.cart), 0)

The setUp method creates a fresh empty list and a sample item before every test. Because setUp runs before each test independently, you never have to worry about one test's changes leaking into another. The test_empty_cart_total test can safely assume the cart starts empty, even if test_add_item ran before it.

For expensive resources like database connections, use setUpClass instead. This class method runs once for the entire test class rather than once per test method.

pythonpython
class TestWithDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.connection = create_test_database()
 
    @classmethod
    def tearDownClass(cls):
        cls.connection.close()

The @classmethod decorator is required for setUpClass and tearDownClass. The connection is created once and shared across all test methods, saving time when setup is expensive.

Running tests from the command line

The unittest module provides a command-line interface with several useful options. The basic command runs all tests in a module:

bashbash
python -m unittest test_shopping_cart.py

Run with the -v flag for verbose output that shows each test name on its own line. You can also narrow the run to a specific class or a single method:

bashbash
python -m unittest test_shopping_cart.TestShoppingCart
python -m unittest test_shopping_cart.TestShoppingCart.test_add_item

The -k flag filters tests by name pattern, so you can run only the tests whose names match a given keyword. This is helpful when you are working on one feature and want fast feedback without waiting for unrelated tests to finish. For example, to run everything related to cart operations without touching other test classes, use this command:

bashbash
python -m unittest -k "cart"

Running python -m unittest without arguments triggers test discovery, which searches the current directory and subdirectories for files matching test*.py and runs every test it finds.

Skipping tests and expected failures

Not every test is ready to run at every moment. The unittest module provides decorators for these situations.

The skip decorator unconditionally skips a test:

pythonpython
@unittest.skip("Payment integration not yet implemented")
def test_process_payment(self):
    result = process_payment(amount=100)
    self.assertTrue(result.success)

The skip decorator is useful when you know in advance that a test cannot pass yet, such as a feature that is still under construction. The skipIf decorator skips a test only when a condition is true, which matters when a test depends on the operating system, a Python version, or an optional dependency:

pythonpython
import sys
 
@unittest.skipIf(sys.platform != "linux", "Requires Linux")
def test_symlink_handling(self):
    result = resolve_symlinks("/path/to/link")
    self.assertIsNotNone(result)

The expectedFailure decorator marks a test known to fail. The test runs, but a failure is reported as expected rather than a regression:

pythonpython
@unittest.expectedFailure
def test_legacy_format(self):
    self.assertEqual(format_date_v1("2026-07-13"), "July 13, 2026")

This decorator is useful when you discover a bug and write a test for it before fixing the underlying code. The test documents the expected fix without breaking your CI pipeline.

Subtests for multiple inputs

When you have several similar inputs to test, writing a separate method for each one creates repetition. The subTest context manager lets you loop over inputs inside a single method while reporting each iteration independently.

pythonpython
class TestNumberClassification(unittest.TestCase):
    def test_even_numbers(self):
        for number in [2, 4, 6, 8, 10]:
            with self.subTest(number=number):
                self.assertEqual(number % 2, 0)

Without subTest, the loop stops at the first failure. With subTest, every iteration runs and every failure is reported independently. The output shows the value of number for each failed iteration, making it easy to identify which input caused the problem.

The subTest call accepts keyword arguments that appear in the failure output. Use descriptive names like input_value or scenario so the failure message tells you exactly which case broke.

Once you understand the unittest module, the next step is to explore pytest, which offers a simpler syntax and a powerful fixture system.

Rune AI

Rune AI

Key Insights

  • unittest.TestCase provides assertion methods like assertEqual, assertTrue, assertRaises, and assertIn that report failures without stopping the test run.
  • setUp and tearDown create fresh test fixtures before and after each test method, ensuring tests do not interfere with each other.
  • python -m unittest discovers and runs all tests in a project automatically when they follow the naming conventions.
  • Use @unittest.skip and @unittest.expectedFailure to handle tests that are not ready to run or represent known bugs.
  • subTest lets you loop over multiple inputs within a single test method while reporting each iteration's result independently.
RunePowered by Rune AI

Frequently Asked Questions

Why does unittest use assertEqual instead of the built-in assert statement?

unittest provides its own assertion methods like assertEqual and assertTrue so the test runner can catch assertion failures without stopping the entire test run. Python's built-in assert raises AssertionError immediately and halts execution. unittest's methods raise a special failure exception that the framework catches and records, allowing it to continue running the remaining tests and produce a complete report.

What is the difference between setUp and setUpClass?

setUp runs before every individual test method and creates a fresh fixture for each test. setUpClass runs once before any tests in the class execute, so all tests share the same class-level fixture. Use setUp for isolated per-test setup like creating a fresh list. Use setUpClass for expensive shared resources like a database connection that every test can reuse.

How do I run only a specific test instead of the whole file?

Pass the module, class, and method name to the unittest command: `python -m unittest test_module.TestClass.test_method`. You can also run a single class with `python -m unittest test_module.TestClass` or use the -k flag to filter by pattern, for example `python -m unittest -k "password"` runs only tests whose names contain the word password.

Conclusion

unittest is a complete testing framework that ships with Python and covers nearly every testing need without third-party dependencies. Its assertion methods, fixture system, test discovery, and skip support give you the tools to build a reliable test suite for any project size.