AlgoInsight

Free lesson · Python Basic to Advance

unittest Basics

Hard7 min readpython

unittest Basics

What unittest Is

unittest is Python's standard library testing framework, modeled after Java's JUnit (the "xUnit" family). Tests are methods on a class that inherits from TestCase. The framework discovers test classes, runs each test method, and reports results.

import unittest

def add(a, b):
    return a + b

class TestAdd(unittest.TestCase):
    def test_positive(self):
        self.assertEqual(add(2, 3), 5)

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

    def test_zero(self):
        self.assertEqual(add(0, 0), 0)

if __name__ == "__main__":
    unittest.main()
This code shows a unittest test class. TestCase methods starting with test underscore become individual tests. Self.assertEqual checks two values are equal. Unittest.main discovers and runs all tests in the file. Each method runs in isolation - setUp and tearDown can prepare or clean up per-test state.

unittest is verbose compared to pytest, but it's always available without installing anything.

Running Tests

Several ways to run unittest:

# Run all tests in a file
python -m unittest test_module.py

# Run a specific test class
python -m unittest test_module.TestAdd

# Run a specific test method
python -m unittest test_module.TestAdd.test_positive

# Discover and run all tests in a directory
python -m unittest discover

# Verbose output
python -m unittest -v test_module.py
python -m unittest test_module.py
python -m unittest discover -s tests
python -m unittest -v test_module.TestAdd
This text shows ways to invoke unittest. Module form runs all discovered tests. Discover walks the directory for test files. The verbose flag prints each test name and status. Specific tests can be addressed by dotted path - module, class, method.

discover is the most common form for running an entire test suite.

Assertion Methods

TestCase provides many assertion methods, each with a specific failure message:

import unittest

class TestAssertions(unittest.TestCase):
    def test_equality(self):
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1, 2)

    def test_truthiness(self):
        self.assertTrue(1)
        self.assertFalse(0)

    def test_identity(self):
        self.assertIs(None, None)
        self.assertIsNot(1, 2)

    def test_membership(self):
        self.assertIn(2, [1, 2, 3])
        self.assertNotIn(4, [1, 2, 3])

    def test_types(self):
        self.assertIsInstance("hello", str)
        self.assertNotIsInstance("hello", int)

    def test_numerical(self):
        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
        self.assertGreater(5, 3)
        self.assertLess(2, 4)
This code shows common assertion methods. Each method describes the relationship being checked - equal, true, in, isinstance, almost equal. The named methods produce clearer error messages than plain assert. Almost equal handles floating-point comparison with explicit precision. Topic 23.3 covers how pytest replaces these with plain assert plus introspection.

The named methods produce diagnostic output on failure - "5 != 6" rather than just AssertionError.

Setup and Teardown

setUp runs before each test method; tearDown runs after:

import unittest

class TestDatabase(unittest.TestCase):
    def setUp(self):
        """Runs before each test."""
        self.db = create_test_database()

    def tearDown(self):
        """Runs after each test, even if it failed."""
        self.db.close()

    def test_insert(self):
        self.db.insert({"name": "alice"})
        self.assertEqual(self.db.count(), 1)

    def test_delete(self):
        self.db.insert({"name": "alice"})
        self.db.delete("alice")
        self.assertEqual(self.db.count(), 0)
This code uses setUp and tearDown. Each test gets a fresh database - setUp creates it, tearDown closes it. The two test methods are independent - each starts from the same clean state. The framework guarantees teardown runs even when the test fails or raises.

For per-class setup (expensive setup shared across tests in one class), setUpClass and tearDownClass are class methods that run once:

import unittest

class TestExpensive(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Runs once before any test in this class."""
        cls.shared_resource = expensive_setup()

    @classmethod
    def tearDownClass(cls):
        cls.shared_resource.close()

    def test_first(self):
        # uses cls.shared_resource (still accessible via self)
        ...
This code shows class-level setup. setUpClass runs once before any test in the class - tearDownClass once after all complete. Useful for expensive setup that can be shared. The trade-off: tests aren't fully isolated; one test can affect another via shared state.

For most code, per-test setUp/tearDown is the safe default. Per-class only when setup cost matters.

Asserting Exceptions

assertRaises checks that code raises an expected exception:

import unittest

def divide(a, b):
    if b == 0:
        raise ValueError("can't divide by zero")
    return a / b

class TestDivide(unittest.TestCase):
    def test_raises_on_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

    def test_raises_with_message(self):
        with self.assertRaisesRegex(ValueError, "can't divide"):
            divide(10, 0)
This code asserts exceptions. The assertRaises context manager passes if the code inside raises the specified exception type. assertRaisesRegex also matches the exception message against a regex. Useful for verifying error handling - that the right error happens for the right input.

For verifying error handling, assertRaises is the right tool - testing that exceptions happen is as important as testing that values are computed.

Skipping and Expected Failures

Tests can be skipped or marked as expected failures:

import unittest
import sys

class TestPlatformSpecific(unittest.TestCase):
    @unittest.skip("temporarily disabled")
    def test_unfinished(self):
        ...

    @unittest.skipIf(sys.platform == "win32", "not supported on Windows")
    def test_unix_only(self):
        ...

    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
    def test_linux_only(self):
        ...

    @unittest.expectedFailure
    def test_known_broken(self):
        self.assertEqual(1, 2)    # expected to fail
This code uses skip decorators. Skip always skips. SkipIf and skipUnless skip based on a condition - platform, environment, feature flag. ExpectedFailure inverts the result - a failing test counts as passing, a passing test counts as failing. Useful for tracking known bugs without breaking the build.

@unittest.skip is preferable to commenting out tests - the test stays visible and the reason for skipping is documented.

Test Discovery

The discover subcommand walks a directory tree looking for test files:

# Standard layout
project/
├── src/
│   └── mymodule/
│       └── __init__.py
└── tests/
    ├── __init__.py
    ├── test_module.py
    └── test_other.py

# Run all tests
python -m unittest discover -s tests
project/
├── src/
│   └── mymodule/
└── tests/
    ├── test_module.py
    └── test_other.py

python -m unittest discover -s tests
This text shows a standard project layout. Tests live in a sibling directory. Discover with the start directory flag finds them. Each test file imports the production code and defines TestCase classes. The naming conventions - test underscore at the start of files and methods - let the framework find tests without explicit registration.

Subtests

For tests that run the same logic with different inputs, subTest runs each variant separately and reports each individually:

import unittest

class TestEvenness(unittest.TestCase):
    def test_even_numbers(self):
        for n in [2, 4, 6, 8, 11]:
            with self.subTest(n=n):
                self.assertEqual(n % 2, 0)
This code uses subTest. Each iteration of the loop runs as a logical subtest. The n=eleven case fails - but the framework reports it as a single failure with context, not a stopping point. Other subtests after it still run. Useful for parameterized assertions without writing many separate test methods. Topic 23.5 covers parametrized testing in more depth via pytest.

subTest lets one method handle multiple cases with individual failure reporting.

Running a Single Test During Development

When debugging one test, running just that test is faster than running the whole suite:

# Just one method
python -m unittest test_module.TestAdd.test_positive

# Just one class
python -m unittest test_module.TestAdd

# Stop on first failure
python -m unittest -f test_module.py

# Verbose
python -m unittest -v test_module.py
python -m unittest test_module.TestAdd.test_positive
python -m unittest -f -v test_module.py
This text shows targeted test runs. Specific tests by dotted path. Failfast flag stops at the first failure - useful when debugging a chain of related failures. Verbose flag shows progress as each test runs. For development iteration, narrow runs and verbose output speed up the feedback loop.

unittest vs pytest

The choice between frameworks:

Aspect unittest pytest
Installation Standard library pip install pytest
Test style Class-based (TestCase) Function-based
Assertions Methods (assertEqual) Plain assert with introspection
Setup setUp/tearDown Fixtures
Parametrization subTest @pytest.mark.parametrize
Plugins Limited Extensive ecosystem

For new code, pytest is usually the cleaner choice. unittest remains common in legacy codebases and is what the Python standard library uses internally.

Performance

unittest test discovery and execution have moderate overhead - milliseconds per test for the framework, plus whatever the test itself does.

For very large test suites (thousands of tests), startup overhead may matter. pytest is generally faster at discovery but the gap is usually small.

Interview Relevance

Interview: unittest basics are foundational Python:

  • The TestCase class and test_* methods
  • Common assertion methods (assertEqual, assertRaises, etc.)
  • setUp and tearDown for per-test setup
  • setUpClass and tearDownClass for shared setup
  • Test discovery with python -m unittest discover
  • When to use skip and expectedFailure decorators
  • The relationship to pytest (covered Topic 23.3)

A common interview moment: writing a basic TestCase for a function. The right answer includes a class, a test_* method, and an assertion.

Another moment: asserting that a function raises an exception. The answer is assertRaises as a context manager.

Module 30 covers Python interview preparation.

Common Pitfalls

Warning: Methods not starting with test_. The framework won't discover them.

Tests depending on setUpClass state without resetting. One test mutates the shared resource; the next sees the mutation.

assertRaises without a context manager. The decorator form exists but is less clear. Prefer the with block.

Asserting equality with == and printing - but not actually using assertEqual. Comparison alone doesn't fail the test.

setUp errors hiding test failures. When setUp raises, the test is marked as error, not failure - different signal.

Forgetting unittest.main() at the bottom. Without it, python test_file.py does nothing.

Floating-point equality with assertEqual. Use assertAlmostEqual for floats.

Try It

Exercise: Practice with unittest:

  1. Write a TestCase with three test_* methods for a function of your choice.
  2. Add setUp and tearDown to create and clean up test state.
  3. Use assertRaises to verify a function raises the right exception for bad input.
  4. Use assertAlmostEqual to test floating-point math.
  5. Add @unittest.skip to one test. Verify it's skipped, not failed.
  6. Use subTest to run the same assertion with multiple inputs.

Back to Python Basic to Advance