Testing in Python: Unit Tests with unittest and pytest

Testing is fundamental to software quality enabling verification that code behaves correctly under various conditions, with unit tests examining individual functions or methods in isolation ensuring components work as expected. Python provides robust testing frameworks including unittest built into the standard library following xUnit patterns with test cases, assertions, and fixtures, and pytest offering modern third-party framework with simpler syntax, powerful fixtures, and extensive plugin ecosystem. Testing frameworks support assertions verifying expected outcomes, test fixtures setting up preconditions and cleanup, mocking isolating code from dependencies, and test-driven development (TDD) writing tests before implementation driving design.
This comprehensive guide explores unittest framework creating test cases inheriting from unittest.TestCase with test methods prefixed with test_, assertions including assertEqual checking equality, assertTrue and assertFalse verifying boolean conditions, assertRaises expecting exceptions, and assertIn checking membership, test fixtures using setUp method running before each test and tearDown cleaning up after tests, running tests with unittest.main() or command-line test discovery, pytest framework with simple assert statements replacing verbose assertion methods, function-based tests without class inheritance, fixtures using @pytest.fixture decorator providing reusable test data, parametrized tests with @pytest.mark.parametrize running same test with multiple inputs, mocking with unittest.mock replacing dependencies with controlled objects using Mock and patch, practical testing patterns following arrange-act-assert structure, test-driven development writing failing tests first then implementing code to pass, and best practices testing edge cases and error conditions, keeping tests independent and isolated, using descriptive test names, organizing tests mirroring source structure, running tests frequently during development, and maintaining test coverage. Whether you're ensuring code reliability through comprehensive test suites, implementing test-driven development for better design, preventing regressions with automated testing, validating edge cases and error handling, or building confidence for refactoring, mastering unittest and pytest provides essential tools for quality assurance enabling robust maintainable code through systematic verification supporting professional Python development from small scripts to large applications.
unittest Framework Basics
The unittest framework provides structured testing through test cases inheriting from unittest.TestCase, test methods starting with test_ prefix, and rich assertion methods. Tests follow arrange-act-assert pattern setting up conditions, executing code, and verifying outcomes. Understanding unittest basics enables systematic testing with Python's standard library.
# unittest Framework Basics
import unittest
# === Code to test ===
def add(a, b):
"""Add two numbers."""
return a + b
def divide(a, b):
"""Divide two numbers."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class Calculator:
"""Simple calculator class."""
def __init__(self):
self.result = 0
def add(self, value):
self.result += value
return self.result
def reset(self):
self.result = 0
# === Basic test case ===
class TestMathFunctions(unittest.TestCase):
"""Test mathematical functions."""
def test_add_positive_numbers(self):
"""Test adding positive numbers."""
result = add(2, 3)
self.assertEqual(result, 5)
def test_add_negative_numbers(self):
"""Test adding negative numbers."""
result = add(-2, -3)
self.assertEqual(result, -5)
def test_add_mixed_numbers(self):
"""Test adding positive and negative."""
self.assertEqual(add(5, -3), 2)
self.assertEqual(add(-5, 3), -2)
# === Assertion methods ===
class TestAssertions(unittest.TestCase):
"""Demonstrate various assertion methods."""
def test_equality(self):
"""Test equality assertions."""
self.assertEqual(2 + 2, 4)
self.assertNotEqual(2 + 2, 5)
def test_boolean(self):
"""Test boolean assertions."""
self.assertTrue(True)
self.assertFalse(False)
self.assertTrue(1 > 0)
def test_none(self):
"""Test None assertions."""
value = None
self.assertIsNone(value)
self.assertIsNotNone("something")
def test_membership(self):
"""Test membership assertions."""
items = [1, 2, 3]
self.assertIn(2, items)
self.assertNotIn(5, items)
def test_type(self):
"""Test type assertions."""
self.assertIsInstance(42, int)
self.assertIsInstance("hello", str)
def test_comparison(self):
"""Test comparison assertions."""
self.assertGreater(5, 3)
self.assertLess(3, 5)
self.assertGreaterEqual(5, 5)
self.assertLessEqual(3, 5)
def test_almost_equal(self):
"""Test floating-point comparison."""
self.assertAlmostEqual(0.1 + 0.2, 0.3)
# === Testing exceptions ===
class TestExceptions(unittest.TestCase):
"""Test exception handling."""
def test_divide_by_zero(self):
"""Test division by zero raises ValueError."""
with self.assertRaises(ValueError):
divide(10, 0)
def test_exception_message(self):
"""Test exception message content."""
with self.assertRaises(ValueError) as context:
divide(10, 0)
self.assertEqual(str(context.exception), "Cannot divide by zero")
def test_no_exception(self):
"""Test normal division doesn't raise exception."""
try:
result = divide(10, 2)
self.assertEqual(result, 5)
except ValueError:
self.fail("divide() raised ValueError unexpectedly")
# === Test fixtures: setUp and tearDown ===
class TestCalculator(unittest.TestCase):
"""Test Calculator class with fixtures."""
def setUp(self):
"""Set up test fixture before each test."""
print("\nSetUp: Creating calculator")
self.calc = Calculator()
def tearDown(self):
"""Clean up after each test."""
print("TearDown: Cleaning up")
self.calc = None
def test_initial_value(self):
"""Test initial calculator value."""
self.assertEqual(self.calc.result, 0)
def test_add_value(self):
"""Test adding value."""
self.calc.add(5)
self.assertEqual(self.calc.result, 5)
def test_multiple_additions(self):
"""Test multiple additions."""
self.calc.add(5)
self.calc.add(3)
self.assertEqual(self.calc.result, 8)
def test_reset(self):
"""Test reset functionality."""
self.calc.add(10)
self.calc.reset()
self.assertEqual(self.calc.result, 0)
# === Class-level fixtures ===
class TestWithClassFixtures(unittest.TestCase):
"""Test with class-level setup/teardown."""
@classmethod
def setUpClass(cls):
"""Run once before all tests in class."""
print("\nsetUpClass: Initializing for all tests")
cls.shared_resource = "shared data"
@classmethod
def tearDownClass(cls):
"""Run once after all tests in class."""
print("tearDownClass: Cleaning up after all tests")
cls.shared_resource = None
def test_one(self):
self.assertEqual(self.shared_resource, "shared data")
def test_two(self):
self.assertEqual(self.shared_resource, "shared data")
# === Skipping tests ===
class TestSkipping(unittest.TestCase):
"""Demonstrate test skipping."""
@unittest.skip("Not implemented yet")
def test_future_feature(self):
"""Test to be implemented."""
pass
@unittest.skipIf(True, "Skipping conditionally")
def test_conditional_skip(self):
"""Skip based on condition."""
pass
@unittest.skipUnless(False, "Only run if condition is true")
def test_skip_unless(self):
"""Run only if condition met."""
pass
# === Running tests ===
if __name__ == '__main__':
# Run all tests
unittest.main()
# Or run specific test suite
# suite = unittest.TestLoader().loadTestsFromTestCase(TestMathFunctions)
# unittest.TextTestRunner(verbosity=2).run(suite)test_ prefix for unittest to discover them. Use descriptive names like test_divide_by_zero_raises_error.pytest Framework and Fixtures
pytest offers modern testing with simpler syntax using plain assert statements, function-based tests without class inheritance, and powerful fixtures providing reusable test data. The @pytest.fixture decorator creates fixtures with automatic dependency injection, while @pytest.mark.parametrize enables testing multiple inputs efficiently. pytest discovers tests automatically and provides detailed failure output.
# pytest Framework and Fixtures
import pytest
# === Code to test ===
def multiply(a, b):
"""Multiply two numbers."""
return a * b
def get_user_by_id(user_id, database):
"""Get user from database."""
return database.get(user_id)
class ShoppingCart:
"""Shopping cart class."""
def __init__(self):
self.items = []
def add_item(self, item, price):
self.items.append({'item': item, 'price': price})
def get_total(self):
return sum(item['price'] for item in self.items)
def clear(self):
self.items = []
# === Basic pytest tests ===
def test_multiply_positive():
"""Test multiplying positive numbers."""
assert multiply(3, 4) == 12
def test_multiply_negative():
"""Test multiplying negative numbers."""
assert multiply(-3, 4) == -12
assert multiply(-3, -4) == 12
def test_multiply_zero():
"""Test multiplying by zero."""
assert multiply(5, 0) == 0
assert multiply(0, 5) == 0
# === Testing exceptions with pytest ===
def divide_pytest(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero_raises():
"""Test division by zero raises ValueError."""
with pytest.raises(ValueError):
divide_pytest(10, 0)
def test_divide_by_zero_message():
"""Test exception message."""
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide_pytest(10, 0)
# === pytest fixtures ===
@pytest.fixture
def sample_cart():
"""Provide a shopping cart for tests."""
cart = ShoppingCart()
cart.add_item("Apple", 1.5)
cart.add_item("Banana", 0.75)
return cart
def test_cart_total(sample_cart):
"""Test cart total calculation."""
assert sample_cart.get_total() == 2.25
def test_cart_add_item(sample_cart):
"""Test adding item to cart."""
sample_cart.add_item("Orange", 2.0)
assert sample_cart.get_total() == 4.25
assert len(sample_cart.items) == 3
# === Fixture with setup and teardown ===
@pytest.fixture
def temp_file(tmp_path):
"""Create temporary file for testing."""
file_path = tmp_path / "test.txt"
file_path.write_text("test content")
yield file_path # Provide file to test
# Teardown: cleanup happens after yield
if file_path.exists():
file_path.unlink()
def test_file_content(temp_file):
"""Test reading file content."""
content = temp_file.read_text()
assert content == "test content"
# === Fixture scopes ===
@pytest.fixture(scope="function") # Default: new for each test
def function_scope_fixture():
print("\nFunction scope: New fixture")
return "function data"
@pytest.fixture(scope="module") # One per test module
def module_scope_fixture():
print("\nModule scope: Shared fixture")
return "module data"
@pytest.fixture(scope="session") # One per test session
def session_scope_fixture():
print("\nSession scope: Global fixture")
return "session data"
# === Parametrized tests ===
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 6),
(5, 4, 20),
(-2, 3, -6),
(0, 10, 0),
(7, 1, 7)
])
def test_multiply_parametrized(a, b, expected):
"""Test multiply with multiple inputs."""
assert multiply(a, b) == expected
@pytest.mark.parametrize("value,expected", [
("hello", True),
("", False),
(None, False),
([1, 2, 3], True),
([], False),
(0, False),
(42, True)
])
def test_truthiness(value, expected):
"""Test boolean conversion."""
assert bool(value) == expected
# === Fixture with parameters ===
@pytest.fixture(params=[1, 2, 3])
def number_fixture(request):
"""Fixture that runs test with multiple values."""
return request.param
def test_with_fixture_params(number_fixture):
"""Test runs 3 times with different values."""
assert number_fixture > 0
# === Combining fixtures ===
@pytest.fixture
def database():
"""Mock database."""
return {
1: {'name': 'Alice', 'age': 30},
2: {'name': 'Bob', 'age': 25}
}
@pytest.fixture
def user_service(database):
"""Service using database fixture."""
def get_user(user_id):
return get_user_by_id(user_id, database)
return get_user
def test_get_user(user_service):
"""Test getting user."""
user = user_service(1)
assert user['name'] == 'Alice'
assert user['age'] == 30
# === Marking tests ===
@pytest.mark.slow
def test_slow_operation():
"""Marked as slow test."""
import time
time.sleep(1)
assert True
@pytest.mark.integration
def test_external_api():
"""Marked as integration test."""
assert True
# Run with: pytest -m slow (only slow tests)
# Run with: pytest -m "not slow" (skip slow tests)
# === Using conftest.py for shared fixtures ===
# In conftest.py file (same directory):
# @pytest.fixture
# def shared_fixture():
# """Available to all test files."""
# return "shared data"
# === Running pytest ===
# Command line:
# pytest # Run all tests
# pytest test_file.py # Run specific file
# pytest test_file.py::test_name # Run specific test
# pytest -v # Verbose output
# pytest -s # Show print statements
# pytest -k "multiply" # Run tests matching pattern
# pytest --collect-only # List all testsassert statements vs unittest's verbose assertion methods. pytest fixtures are more flexible than unittest's setUp/tearDown.Mocking and Test Isolation
Mocking replaces real dependencies with controlled test doubles isolating code under test from external systems. The unittest.mock module provides Mock objects recording how they're called, patch decorator replacing objects temporarily, and MagicMock supporting magic methods. Mocking enables testing code depending on databases, APIs, or file systems without actual resources.
# Mocking and Test Isolation
from unittest.mock import Mock, MagicMock, patch, call
import pytest
# === Code with external dependencies ===
import requests
def fetch_user_data(user_id):
"""Fetch user data from API."""
response = requests.get(f'https://api.example.com/users/{user_id}')
if response.status_code == 200:
return response.json()
return None
def send_email(to, subject, body):
"""Send email (external service)."""
# Imagine this calls external email service
pass
class UserService:
"""Service with external dependencies."""
def __init__(self, database):
self.database = database
def get_user(self, user_id):
return self.database.find_user(user_id)
def create_user(self, name, email):
user_id = self.database.insert_user(name, email)
send_email(email, "Welcome", f"Hello {name}!")
return user_id
# === Basic Mock usage ===
def test_mock_basics():
"""Demonstrate basic Mock usage."""
# Create mock object
mock = Mock()
# Call mock
mock.some_method('arg1', 'arg2')
mock.another_method(key='value')
# Verify calls
mock.some_method.assert_called_once_with('arg1', 'arg2')
mock.another_method.assert_called_with(key='value')
assert mock.some_method.call_count == 1
def test_mock_return_value():
"""Test mock return values."""
mock = Mock()
mock.return_value = 42
result = mock()
assert result == 42
# Method return value
mock.get_value.return_value = 'test'
assert mock.get_value() == 'test'
# === Patching external dependencies ===
@patch('requests.get')
def test_fetch_user_data(mock_get):
"""Test fetching user data with mocked requests."""
# Configure mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'id': 1,
'name': 'Alice',
'email': '[email protected]'
}
mock_get.return_value = mock_response
# Call function
user = fetch_user_data(1)
# Verify results
assert user['name'] == 'Alice'
assert user['email'] == '[email protected]'
# Verify mock was called correctly
mock_get.assert_called_once_with('https://api.example.com/users/1')
@patch('requests.get')
def test_fetch_user_not_found(mock_get):
"""Test handling 404 response."""
mock_response = Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
user = fetch_user_data(999)
assert user is None
# === Patching with context manager ===
def test_with_context_manager():
"""Test using patch as context manager."""
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'id': 1, 'name': 'Bob'}
mock_get.return_value = mock_response
user = fetch_user_data(1)
assert user['name'] == 'Bob'
# === Mocking class dependencies ===
def test_user_service_with_mock():
"""Test UserService with mocked database."""
# Create mock database
mock_db = Mock()
mock_db.find_user.return_value = {
'id': 1,
'name': 'Alice',
'email': '[email protected]'
}
# Create service with mock
service = UserService(mock_db)
# Test method
user = service.get_user(1)
assert user['name'] == 'Alice'
# Verify database was called
mock_db.find_user.assert_called_once_with(1)
@patch('__main__.send_email')
def test_create_user_sends_email(mock_send_email):
"""Test that creating user sends email."""
mock_db = Mock()
mock_db.insert_user.return_value = 123
service = UserService(mock_db)
user_id = service.create_user('Alice', '[email protected]')
# Verify email was sent
mock_send_email.assert_called_once_with(
'[email protected]',
'Welcome',
'Hello Alice!'
)
assert user_id == 123
# === Mock side effects ===
def test_mock_side_effect():
"""Test mock with side effects."""
mock = Mock()
# Return different values on successive calls
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
# Raise exception
mock.side_effect = ValueError("Error")
with pytest.raises(ValueError):
mock()
# === MagicMock for magic methods ===
def test_magic_mock():
"""Test MagicMock supporting magic methods."""
mock = MagicMock()
# Supports __getitem__
mock.__getitem__.return_value = 'value'
assert mock['key'] == 'value'
# Supports __len__
mock.__len__.return_value = 5
assert len(mock) == 5
# Supports iteration
mock.__iter__.return_value = iter([1, 2, 3])
assert list(mock) == [1, 2, 3]
# === Patch multiple ===
@patch('requests.get')
@patch('__main__.send_email')
def test_multiple_patches(mock_email, mock_get):
"""Test with multiple patches."""
# Note: patches applied bottom-to-top
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'name': 'Test'}
mock_get.return_value = mock_response
user = fetch_user_data(1)
assert user['name'] == 'Test'
# === Spec for strict mocking ===
class RealDatabase:
def find_user(self, user_id):
pass
def insert_user(self, name, email):
pass
def test_mock_with_spec():
"""Test mock matching real interface."""
# Mock only allows methods from RealDatabase
mock_db = Mock(spec=RealDatabase)
# This works
mock_db.find_user(1)
# This raises AttributeError
with pytest.raises(AttributeError):
mock_db.non_existent_method()
# === Verifying call order ===
def test_call_order():
"""Test method call order."""
mock = Mock()
mock.method_a()
mock.method_b()
mock.method_c()
# Verify call order
expected_calls = [
call.method_a(),
call.method_b(),
call.method_c()
]
assert mock.mock_calls == expected_calls
# === Partial mocking ===
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
def test_partial_mock():
"""Test mocking specific methods only."""
calc = Calculator()
with patch.object(calc, 'multiply', return_value=100):
# multiply is mocked
assert calc.multiply(5, 10) == 100
# add still works normally
assert calc.add(5, 10) == 15Testing Best Practices
- Follow arrange-act-assert pattern: Structure tests in three phases: arrange setup, act execute code, assert verify outcomes. This makes tests clear and maintainable
- Keep tests independent: Each test should run independently without depending on other tests. Use fixtures for setup, not test execution order
- Test edge cases and errors: Don't just test happy paths. Test boundary conditions, invalid inputs, empty collections, null values, and error handling
- Use descriptive test names: Name tests clearly describing what they test:
test_divide_by_zero_raises_value_errorbetter thantest_divide - One assertion per test (guideline): Focus each test on single behavior. Multiple related assertions acceptable, but testing unrelated things makes debugging harder
- Mock external dependencies: Mock APIs, databases, file systems, network calls. Tests should be fast, reliable, and runnable offline
- Organize tests mirroring source: Structure test directories matching source code.
src/myapp/module.pytested bytests/test_module.py - Run tests frequently: Run tests during development, before commits, in CI/CD pipelines. Fast feedback catches bugs early reducing debugging time
- Maintain high test coverage: Aim for 80%+ code coverage but prioritize quality over quantity. Test critical paths thoroughly over arbitrary coverage goals
- Practice test-driven development: Write failing test first, implement code to pass, refactor. TDD improves design, ensures testability, and prevents regressions
Conclusion
Testing ensures code reliability through systematic verification that functions and methods behave correctly under various conditions, with unittest providing standard library framework following xUnit patterns through test cases inheriting from unittest.TestCase with methods prefixed test_, rich assertion methods including assertEqual, assertTrue, assertRaises checking expected outcomes, and fixtures using setUp running before each test and tearDown cleaning up afterward. The unittest framework supports class-level fixtures with setUpClass and tearDownClass running once per test class, test skipping with decorators for conditional execution, test suites grouping related tests, and command-line test discovery finding tests automatically, with tests run using unittest.main() or test runners providing detailed output showing passes, failures, and errors.
pytest offers modern testing with simpler syntax using plain assert statements replacing verbose assertion methods, function-based tests without requiring class inheritance, powerful fixtures created with @pytest.fixture decorator providing reusable test data with automatic dependency injection and flexible scoping, parametrized tests using @pytest.mark.parametrize running same test with multiple inputs efficiently, and automatic test discovery with detailed failure output. Mocking isolates code under test from external dependencies using unittest.mock providing Mock objects recording calls and configuring return values, patch decorator temporarily replacing objects during tests, MagicMock supporting magic methods like __getitem__ and __len__, and spec parameter ensuring mocks match real interfaces preventing invalid method calls. Best practices emphasize following arrange-act-assert pattern structuring tests clearly, keeping tests independent without execution order dependencies, testing edge cases and error conditions beyond happy paths, using descriptive test names explaining what tests verify, focusing tests on single behaviors, mocking external dependencies for fast reliable tests, organizing test structure mirroring source code, running tests frequently during development and in CI/CD, maintaining high test coverage prioritizing critical paths, and practicing test-driven development writing tests first driving better design. By mastering unittest framework for structured testing with standard library, pytest for modern simpler testing with powerful fixtures, mocking techniques isolating code from dependencies, test organization and naming conventions, and best practices including TDD methodology, you gain essential tools for quality assurance enabling robust maintainable code through systematic verification, preventing regressions through automated testing, building confidence for refactoring, ensuring edge case handling, and supporting professional Python development from small utilities to large applications requiring comprehensive test coverage.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


