unittest is Python's built-in testing framework. Here's how to use it.

Basic Test

import unittest
 
def add(a, b):
    return a + b
 
class TestAdd(unittest.TestCase):
    def test_add_positive(self):
        self.assertEqual(add(2, 3), 5)
    
    def test_add_negative(self):
        self.assertEqual(add(-1, 1), 0)
 
if __name__ == "__main__":
    unittest.main()

Run:

python -m unittest test_module.py
python -m unittest discover  # Find all tests

Assertions

class TestAssertions(unittest.TestCase):
    def test_equality(self):
        self.assertEqual(a, b)       # a == b
        self.assertNotEqual(a, b)    # a != b
    
    def test_truth(self):
        self.assertTrue(x)           # bool(x) is True
        self.assertFalse(x)          # bool(x) is False
    
    def test_identity(self):
        self.assertIs(a, b)          # a is b
        self.assertIsNot(a, b)       # a is not b
        self.assertIsNone(x)         # x is None
        self.assertIsNotNone(x)      # x is not None
    
    def test_membership(self):
        self.assertIn(a, b)          # a in b
        self.assertNotIn(a, b)       # a not in b
    
    def test_types(self):
        self.assertIsInstance(a, T)  # isinstance(a, T)
    
    def test_comparison(self):
        self.assertGreater(a, b)     # a > b
        self.assertLess(a, b)        # a < b
        self.assertGreaterEqual(a, b)
        self.assertLessEqual(a, b)
    
    def test_approximate(self):
        self.assertAlmostEqual(a, b, places=2)

Testing Exceptions

class TestExceptions(unittest.TestCase):
    def test_raises(self):
        with self.assertRaises(ValueError):
            int("not a number")
    
    def test_raises_with_message(self):
        with self.assertRaises(ValueError) as context:
            raise ValueError("custom message")
        self.assertIn("custom", str(context.exception))
    
    def test_raises_regex(self):
        with self.assertRaisesRegex(ValueError, r"invalid.*"):
            raise ValueError("invalid input")

setUp and tearDown

class TestDatabase(unittest.TestCase):
    def setUp(self):
        """Run before each test."""
        self.db = Database(":memory:")
        self.db.create_tables()
    
    def tearDown(self):
        """Run after each test."""
        self.db.close()
    
    def test_insert(self):
        self.db.insert({"name": "Alice"})
        self.assertEqual(self.db.count(), 1)
 
class TestWithClassSetup(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Run once before all tests in class."""
        cls.expensive_resource = load_data()
    
    @classmethod
    def tearDownClass(cls):
        """Run once after all tests in class."""
        cls.expensive_resource.cleanup()

Skipping Tests

import sys
 
class TestSkipping(unittest.TestCase):
    @unittest.skip("Not implemented yet")
    def test_future_feature(self):
        pass
    
    @unittest.skipIf(sys.platform == "win32", "Unix only")
    def test_unix_specific(self):
        pass
    
    @unittest.skipUnless(sys.platform == "darwin", "macOS only")
    def test_macos_specific(self):
        pass
    
    @unittest.expectedFailure
    def test_known_bug(self):
        self.assertEqual(1, 2)  # Won't fail the suite

Test Discovery

tests/
├── __init__.py
├── test_users.py
├── test_orders.py
└── integration/
    ├── __init__.py
    └── test_api.py
# Run all tests
python -m unittest discover
 
# Specific directory
python -m unittest discover -s tests
 
# Pattern
python -m unittest discover -p "test_*.py"

Mocking

from unittest.mock import Mock, patch, MagicMock
 
class TestMocking(unittest.TestCase):
    def test_mock_object(self):
        mock = Mock()
        mock.method.return_value = 42
        
        result = mock.method("arg")
        
        self.assertEqual(result, 42)
        mock.method.assert_called_once_with("arg")
    
    @patch("module.external_api")
    def test_patch_decorator(self, mock_api):
        mock_api.fetch.return_value = {"data": []}
        result = function_using_api()
        mock_api.fetch.assert_called()
    
    def test_patch_context(self):
        with patch("module.requests.get") as mock_get:
            mock_get.return_value.json.return_value = {}
            result = fetch_data()

Mock Assertions

mock = Mock()
mock("arg1", key="value")
 
mock.assert_called()
mock.assert_called_once()
mock.assert_called_with("arg1", key="value")
mock.assert_called_once_with("arg1", key="value")
mock.assert_not_called()
 
# Check call count
self.assertEqual(mock.call_count, 1)
 
# Check all calls
mock.assert_has_calls([
    call("arg1"),
    call("arg2"),
])

Parameterized Tests

class TestMath(unittest.TestCase):
    def test_add_cases(self):
        cases = [
            (2, 3, 5),
            (-1, 1, 0),
            (0, 0, 0),
        ]
        for a, b, expected in cases:
            with self.subTest(a=a, b=b):
                self.assertEqual(add(a, b), expected)

Test Organization

# test_user.py
import unittest
from myapp.user import User
 
class TestUserCreation(unittest.TestCase):
    """Tests for user creation."""
    
    def test_create_with_name(self):
        user = User("Alice")
        self.assertEqual(user.name, "Alice")
    
    def test_create_requires_name(self):
        with self.assertRaises(ValueError):
            User("")
 
class TestUserAuthentication(unittest.TestCase):
    """Tests for user authentication."""
    
    def setUp(self):
        self.user = User("Alice")
        self.user.set_password("secret")
    
    def test_correct_password(self):
        self.assertTrue(self.user.check_password("secret"))
    
    def test_wrong_password(self):
        self.assertFalse(self.user.check_password("wrong"))

Running Tests

# Run all tests
python -m unittest
 
# Specific module
python -m unittest test_module
 
# Specific class
python -m unittest test_module.TestClass
 
# Specific test
python -m unittest test_module.TestClass.test_method
 
# Verbose output
python -m unittest -v
 
# Stop on first failure
python -m unittest -f

Quick Reference

import unittest
from unittest.mock import Mock, patch
 
class TestExample(unittest.TestCase):
    def setUp(self):
        # Before each test
        pass
    
    def tearDown(self):
        # After each test
        pass
    
    def test_something(self):
        self.assertEqual(a, b)
        self.assertTrue(x)
        self.assertRaises(Error, func)
        
        with self.assertRaises(Error):
            func()
    
    @patch("module.func")
    def test_with_mock(self, mock_func):
        mock_func.return_value = 42
        # Test code
 
if __name__ == "__main__":
    unittest.main()

unittest is always available. For more features, consider pytest, but unittest handles most testing needs.

React to this post: