Free lesson · Python Basic to Advance
unittest Basics
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:
- By default, looks for files named
test*.py - Within those files, looks for
TestCasesubclasses - Within those classes, looks for methods named
test_*
# 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:
unittestbasics are foundational Python:
- The
TestCaseclass andtest_*methods- Common assertion methods (
assertEqual,assertRaises, etc.)setUpandtearDownfor per-test setupsetUpClassandtearDownClassfor 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
TestCasefor a function. The right answer includes a class, atest_*method, and an assertion.Another moment: asserting that a function raises an exception. The answer is
assertRaisesas 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
setUpClassstate without resetting. One test mutates the shared resource; the next sees the mutation.
assertRaiseswithout a context manager. The decorator form exists but is less clear. Prefer thewithblock.Asserting equality with
==and printing - but not actually usingassertEqual. Comparison alone doesn't fail the test.
setUperrors 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.pydoes nothing.Floating-point equality with
assertEqual. UseassertAlmostEqualfor floats.
Try It
Exercise: Practice with
unittest:
- Write a
TestCasewith threetest_*methods for a function of your choice.- Add
setUpandtearDownto create and clean up test state.- Use
assertRaisesto verify a function raises the right exception for bad input.- Use
assertAlmostEqualto test floating-point math.- Add
@unittest.skipto one test. Verify it's skipped, not failed.- Use
subTestto run the same assertion with multiple inputs.