Unit Testing with unittest
Python’s unittest module is the standard library’s answer to automated testing. Inspired by JUnit, it provides a complete framework for writing, discovering, and running tests. While pytest has become more popular in recent years, understanding unittest is valuable—it ships with Python, works without dependencies, and its patterns appear throughout the testing ecosystem.
This guide walks you through writing tests with unittest from scratch.
Why Use unittest?
The unittest framework offers several advantages:
- No dependencies — comes with Python’s standard library
- Test discovery — automatically finds and runs tests
- Object-oriented — organizes tests in classes for better structure
- Portable — works the same across all Python implementations
Many developers start with unittest and later explore pytest. The core concepts transfer easily between frameworks.
Your First Test
Create a file called test_calculator.py:
import unittest
def add(a, b):
return a + b
def subtract(a, b):
return a - b
class TestCalculator(unittest.TestCase):
def test_add_two_positive_numbers(self):
result = add(3, 5)
self.assertEqual(result, 8)
def test_add_negative_numbers(self):
result = add(-1, -1)
self.assertEqual(result, -2)
def test_subtract(self):
result = subtract(10, 4)
self.assertEqual(result, 6)
if __name__ == "__main__":
unittest.main()
Run the tests:
python test_calculator.py
# output: ...
# output: Ran 3 tests in 0.001s
# output: OK
Understanding TestCase
The TestCase class is the foundation. Each test method must start with test_ — this convention lets unittest discover your tests automatically.
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual("hello".upper(), "HELLO")
def test_isupper(self):
self.assertTrue("HELLO".isupper())
self.assertFalse("Hello".isupper())
def test_split(self):
s = "hello world"
self.assertEqual(s.split(), ["hello", "world"])
setUp and tearDown
These special methods run before and after each test:
class TestDatabase(unittest.TestCase):
def setUp(self):
self.db = Database.connect("test.db")
self.db.seed()
def tearDown(self):
self.db.cleanup()
def test_insert(self):
self.db.insert("test", {"name": "Alice"})
result = self.db.find("test")
self.assertIsNotNone(result)
This ensures each test starts with a clean database state.
Class-Level Setup
Use setUpClass and tearDownClass for expensive setup that should run once:
class TestExpensiveResource(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.connection = ExpensiveConnection.connect()
@classmethod
def tearDownClass(cls):
cls.connection.close()
def test_something(self):
# uses cls.connection
pass
Note: these require the @classmethod decorator.
Assertion Methods
TestCase provides many assertion methods beyond the basic assertEqual:
| Method | Use Case |
|---|---|
assertEqual(a, b) | Check a == b |
assertNotEqual(a, b) | Check a != b |
assertTrue(x) | Check x is truthy |
assertFalse(x) | Check x is falsy |
assertIsNone(x) | Check x is None |
assertIsNotNone(x) | Check x is not None |
assertIn(a, b) | Check a in b |
assertNotIn(a, b) | Check a not in b |
assertIs(a, b) | Check a is b (identity) |
assertIsInstance(a, b) | Check isinstance(a, b) |
assertRaises(exc) | Check exception is raised |
assertRaises
Test that code raises an exception:
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
x = 1 / 0
With additional arguments:
def test_value_error(self):
with self.assertRaises(ValueError) as context:
int("not a number")
self.assertIn("invalid literal", str(context.exception))
assertAlmostEqual
For floating-point comparisons:
def test_float_precision(self):
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
# Or with delta:
self.assertAlmostEqual(10.0, 9.99, delta=0.1)
Running Tests
From Command Line
# Run all tests in current module
python -m unittest
# Verbose output
python -m unittest -v
# Run specific test file
python -m unittest test_calculator
# Run specific test class
python -m unittest test_calculator.TestCalculator
# Run specific test method
python -m unittest test_calculator.TestCalculator.test_add
# Discover all tests in directory
python -m unittest discover
From Code
# Option 1: unittest.main() at module end
if __name__ == "__main__":
unittest.main()
# Option 2: Programmatic test runner
import unittest
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestCalculator)
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
{{ test run }}
{{ failures }}
{{ errors }}
Test Suites
Group multiple test classes or modules into a suite:
import unittest
# Build a suite manually
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTest(TestMath("test_add"))
suite.addTests(loader.loadTestsFromTestCase(TestStrings))
suite.addTests(loader.loadTestsFromName("myapp.tests"))
# Run the suite
runner = unittest.TextTestRunner(verbosity=1)
runner.run(suite)
Skip Tests
Skip tests conditionally or unconditionally:
class TestFeature(unittest.TestCase):
@unittest.skip("Not implemented yet")
def test_future_feature(self):
pass
@unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+")
def test_new_feature(self):
pass
@unittest.skipUnless(os.name == "posix", "Unix only")
def test_unix_feature(self):
pass
Mocking with unittest.mock
The unittest.mock submodule provides powerful mocking capabilities:
from unittest.mock import Mock, patch, MagicMock
# Create a mock
mock_obj = Mock()
mock_obj.method.return_value = 42
result = mock_obj.method()
print(result) # 42
# Check calls
mock_obj.method.assert_called_once()
mock_obj.method.assert_called_with(42)
# Patch external dependencies
@patch("mymodule.requests.get")
def test_api_call(mock_get):
mock_get.return_value = Mock(status_code=200, json={"key": "value"})
result = call_api()
self.assertEqual(result, {"key": "value"})
# MagicMock for complex objects
mock_list = MagicMock()
mock_list.__len__.return_value = 5
print(len(mock_list)) # 5
Mocking Classes
from unittest.mock import patch, MagicMock
# Patch at the class definition
@patch("mymodule.Database")
def test_with_mock_db(MockDatabase):
# Configure mock
instance = MagicMock()
instance.query.return_value = [{"id": 1, "name": "Alice"}]
MockDatabase.return_value = instance
# Test code uses the mock
result = fetch_users()
self.assertEqual(len(result), 1)
Best Practices
Keep Tests Independent
Each test should work in isolation:
class TestUserManager(unittest.TestCase):
def setUp(self):
self.manager = UserManager()
def test_create_user(self):
user = self.manager.create("alice", "alice@example.com")
self.assertEqual(user.name, "alice")
def test_duplicate_email_fails(self):
self.manager.create("alice", "alice@example.com")
with self.assertRaises(DuplicateEmailError):
self.manager.create("bob", "alice@example.com")
Name Tests Descriptively
# Good
def test_division_by_zero_raises_zero_division_error(self):
# Bad
def test_div0(self):
Test One Thing Per Method
# Good - focused
def test_user_name_is_required(self):
with self.assertRaises(ValidationError):
User(name=None)
def test_user_email_must_be_valid(self):
with self.assertRaises(ValidationError):
User(email="not-an-email")
Test Edge Cases
def test_list_index_out_of_bounds(self):
with self.assertRaises(IndexError):
lst = [1, 2, 3]
x = lst[100]
unittest vs pytest
Both are valid choices. Here’s when to use each:
| Feature | unittest | pytest |
|---|---|---|
| Dependencies | None | pytest package |
| Syntax | assert methods | Plain assert |
| Fixtures | setUp/tearDown | @pytest.fixture |
| Output | Traditional | Detailed, helpful |
| Plugins | Limited | Rich ecosystem |
Many projects use unittest for its simplicity and no dependencies. pytest shines for complex testing scenarios with its rich plugin ecosystem.
Conclusion
The unittest module provides everything you need for basic to intermediate testing. Its integration with the standard library means you can start testing immediately without installing anything. Understanding unittest also makes it easier to learn pytest later, since many concepts transfer directly.
Start with simple tests, organize them in classes, and gradually add fixtures and mocking as your tests grow more complex.
See Also
- pytest-basics — Getting started with pytest, the popular alternative testing framework
- mocking-with-pytest — Advanced mocking techniques for tests
- error-handling — Understanding Python exceptions used in tests
- unittest-module — Complete reference for the unittest module