Unit Testing with unittest

· 5 min read · Updated March 13, 2026 · beginner
unittest testing unit-testing test-framework python-builtins

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:

MethodUse 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:

Featureunittestpytest
DependenciesNonepytest package
Syntaxassert methodsPlain assert
FixturessetUp/tearDown@pytest.fixture
OutputTraditionalDetailed, helpful
PluginsLimitedRich 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