Python unittest.mock: Mocking Objects and Functions

Unit tests should test units in isolation. When your function calls an external API, queries a database, or reads from the filesystem, you're no longer testing your code—you're testing the entire...

Key Insights

  • Mock where the object is used, not where it’s defined—this is the most common source of “why isn’t my mock working?” frustration
  • Use MagicMock by default unless you have a specific reason to use Mock; it handles Python’s magic methods automatically
  • Always prefer autospec=True when patching to catch signature mismatches that would otherwise silently pass your tests

Introduction to Mocking

Unit tests should test units in isolation. When your function calls an external API, queries a database, or reads from the filesystem, you’re no longer testing your code—you’re testing the entire system. Mocking solves this by replacing real objects with controlled substitutes that behave exactly how you specify.

Python’s unittest.mock module (part of the standard library since Python 3.3) provides everything you need. It’s powerful, flexible, and occasionally confusing. This article covers the patterns you’ll actually use in production code.

The core principle is simple: replace the thing you don’t control with something you do control. Your tests become faster, more reliable, and truly isolated.

The Mock and MagicMock Classes

The Mock class is the foundation of unittest.mock. It creates objects that record how they’re used and let you specify their behavior.

from unittest.mock import Mock, MagicMock

# Basic Mock usage
api_client = Mock()
api_client.get_user.return_value = {"id": 1, "name": "Alice"}

# Use the mock
result = api_client.get_user(user_id=1)
print(result)  # {"id": 1, "name": "Alice"}

# Verify the call
api_client.get_user.assert_called_once_with(user_id=1)

MagicMock extends Mock with default implementations of magic methods like __len__, __iter__, and __getitem__. Use MagicMock unless you specifically need to test that magic methods aren’t called.

# Mock doesn't handle magic methods by default
regular_mock = Mock()
# len(regular_mock)  # TypeError!

# MagicMock does
magic_mock = MagicMock()
magic_mock.__len__.return_value = 5
print(len(magic_mock))  # 5

# Iteration works too
magic_mock.__iter__.return_value = iter([1, 2, 3])
print(list(magic_mock))  # [1, 2, 3]

Key attributes you’ll use constantly:

  • return_value: What the mock returns when called
  • side_effect: Exceptions to raise, values to return sequentially, or a function to call
  • call_args: The arguments from the most recent call
  • call_count: How many times the mock was called

Using the @patch Decorator

The @patch decorator temporarily replaces an object during a test. The critical concept is patch where the object is used, not where it’s defined.

# services/user_service.py
from external_api import APIClient

def get_user_profile(user_id):
    client = APIClient()
    user_data = client.fetch_user(user_id)
    return {
        "display_name": user_data["name"].upper(),
        "email": user_data["email"]
    }
# tests/test_user_service.py
from unittest.mock import patch, MagicMock
from services.user_service import get_user_profile

class TestGetUserProfile:
    # Patch where APIClient is USED (services.user_service)
    # NOT where it's defined (external_api)
    @patch('services.user_service.APIClient')
    def test_returns_formatted_profile(self, mock_api_class):
        # Configure the mock instance
        mock_client = MagicMock()
        mock_client.fetch_user.return_value = {
            "name": "alice",
            "email": "alice@example.com"
        }
        mock_api_class.return_value = mock_client
        
        result = get_user_profile(123)
        
        assert result == {
            "display_name": "ALICE",
            "email": "alice@example.com"
        }
        mock_client.fetch_user.assert_called_once_with(123)

The context manager form is useful when you only need to patch part of a test:

def test_handles_api_failure(self):
    # Setup phase without mocking
    user_cache = UserCache()
    
    # Only mock during the specific operation
    with patch('services.user_service.APIClient') as mock_api_class:
        mock_client = MagicMock()
        mock_client.fetch_user.side_effect = ConnectionError("API down")
        mock_api_class.return_value = mock_client
        
        result = get_user_profile(123)
        
    assert result is None  # Or however you handle failures

Patching Object Attributes and Methods

When you need to mock a specific method on an existing class rather than the whole class, use patch.object():

# database/repository.py
class UserRepository:
    def __init__(self, connection):
        self.connection = connection
    
    def find_by_id(self, user_id):
        cursor = self.connection.execute(
            "SELECT * FROM users WHERE id = ?", (user_id,)
        )
        return cursor.fetchone()
    
    def save(self, user):
        self.connection.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            (user["name"], user["email"])
        )
        self.connection.commit()
# tests/test_repository.py
from unittest.mock import patch, MagicMock
from database.repository import UserRepository

class TestUserRepository:
    def test_find_by_id_returns_user(self):
        mock_connection = MagicMock()
        mock_cursor = MagicMock()
        mock_cursor.fetchone.return_value = (1, "Alice", "alice@example.com")
        mock_connection.execute.return_value = mock_cursor
        
        repo = UserRepository(mock_connection)
        result = repo.find_by_id(1)
        
        assert result == (1, "Alice", "alice@example.com")
        mock_connection.execute.assert_called_once_with(
            "SELECT * FROM users WHERE id = ?", (1,)
        )

    @patch.object(UserRepository, 'find_by_id')
    def test_service_uses_repository(self, mock_find):
        mock_find.return_value = {"id": 1, "name": "Alice"}
        
        repo = UserRepository(None)  # Connection doesn't matter
        user = repo.find_by_id(1)
        
        assert user["name"] == "Alice"

For multiple patches, stack the decorators (note the reversed order of arguments):

@patch('module.ClassC')
@patch('module.ClassB')
@patch('module.ClassA')
def test_multiple_patches(self, mock_a, mock_b, mock_c):
    # mock_a corresponds to ClassA (innermost decorator)
    # mock_c corresponds to ClassC (outermost decorator)
    pass

Controlling Mock Behavior with side_effect

The side_effect attribute is your tool for complex mock behavior. It handles three scenarios:

Raising exceptions:

from unittest.mock import Mock
import pytest

def test_handles_connection_error():
    http_client = Mock()
    http_client.get.side_effect = ConnectionError("Network unreachable")
    
    with pytest.raises(ConnectionError):
        http_client.get("https://api.example.com/users")

Returning different values on successive calls:

def test_retry_logic():
    http_client = Mock()
    # First two calls fail, third succeeds
    http_client.get.side_effect = [
        ConnectionError("Timeout"),
        ConnectionError("Timeout"),
        {"status": 200, "data": "success"}
    ]
    
    # Your retry logic should handle this
    result = fetch_with_retry(http_client, "/api/data", max_retries=3)
    
    assert result["status"] == 200
    assert http_client.get.call_count == 3

Using a custom function:

def test_dynamic_responses():
    def mock_fetch(url):
        if "users" in url:
            return {"type": "user", "count": 10}
        elif "posts" in url:
            return {"type": "post", "count": 50}
        raise ValueError(f"Unknown endpoint: {url}")
    
    api = Mock()
    api.fetch.side_effect = mock_fetch
    
    assert api.fetch("/api/users")["type"] == "user"
    assert api.fetch("/api/posts")["count"] == 50

Assertions and Call Inspection

Mocks record every interaction. Use this for precise verification:

from unittest.mock import Mock, call, ANY

def test_logging_calls():
    logger = Mock()
    
    # Simulate code that logs
    logger.info("Starting process")
    logger.debug("Processing item", extra={"item_id": 42})
    logger.info("Process complete")
    
    # Verify specific calls
    logger.info.assert_any_call("Starting process")
    
    # Verify call count
    assert logger.info.call_count == 2
    assert logger.debug.call_count == 1
    
    # Verify exact call sequence
    logger.info.assert_has_calls([
        call("Starting process"),
        call("Process complete")
    ])
    
    # Use ANY for flexible matching
    logger.debug.assert_called_with("Processing item", extra=ANY)

Inspect call arguments directly when assertions aren’t flexible enough:

def test_call_inspection():
    notifier = Mock()
    
    notifier.send("user@example.com", subject="Hello", priority=1)
    notifier.send("admin@example.com", subject="Alert", priority=5)
    
    # Get all calls
    all_calls = notifier.send.call_args_list
    
    # Find high-priority calls
    high_priority = [
        c for c in all_calls 
        if c.kwargs.get("priority", 0) > 3
    ]
    assert len(high_priority) == 1
    assert high_priority[0].args[0] == "admin@example.com"

Best Practices and Common Pitfalls

Use autospec for safer mocks. Without it, mocks accept any arguments, hiding bugs:

# The real class
class EmailService:
    def send(self, to, subject, body):
        pass

# Without autospec - this passes but is WRONG
def test_without_autospec():
    mock_service = Mock()
    mock_service.send("wrong", "args")  # No error!
    mock_service.send.assert_called()  # Passes

# With autospec - catches the error
from unittest.mock import create_autospec

def test_with_autospec():
    mock_service = create_autospec(EmailService)
    # mock_service.send("wrong", "args")  # TypeError: missing required argument 'body'
    mock_service.send("to@example.com", "Subject", "Body")  # Correct

Or use autospec=True with @patch:

@patch('services.email.EmailService', autospec=True)
def test_email_sent(self, mock_service_class):
    mock_instance = mock_service_class.return_value
    # Now mock_instance.send() enforces the real signature

Don’t over-mock. If you’re mocking more than you’re testing, reconsider your design. Mocks should isolate external dependencies, not replace the code you’re trying to test.

Reset mocks when reusing them:

def test_multiple_scenarios():
    api = Mock()
    
    api.call("first")
    assert api.call.call_count == 1
    
    api.reset_mock()  # Clear all call history
    
    api.call("second")
    assert api.call.call_count == 1  # Count reset

Prefer dependency injection over patching. If your code accepts dependencies as parameters, you can pass mocks directly without patching:

# Better: accepts dependency
def process_order(order, payment_gateway):
    payment_gateway.charge(order.total)

# Test is simpler
def test_process_order():
    mock_gateway = Mock()
    order = Order(total=100)
    
    process_order(order, mock_gateway)
    
    mock_gateway.charge.assert_called_once_with(100)

Mocking is a tool, not a goal. Use it to isolate your code from things you can’t control, verify interactions with dependencies, and test error handling. But always remember: the best test is one that exercises real code paths with minimal mocking.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.