Real tests don't hit production databases. They don't call external APIs. They don't send actual emails. Mocking lets you replace these dependencies with controlled substitutes.

The Problem

Consider this function:

def get_user_weather(user_id):
    user = database.get_user(user_id)
    weather = requests.get(f"https://api.weather.com/{user.city}")
    return f"{user.name}'s weather: {weather.json()['temp']}°F"

Testing this directly would require a database, network access, and a weather API key. That's fragile and slow. Instead, we mock.

unittest.mock Basics

Python's built-in unittest.mock module provides everything you need:

from unittest.mock import Mock, patch, MagicMock
 
# Create a simple mock
mock_user = Mock()
mock_user.name = "Alice"
mock_user.city = "Seattle"
 
# Mock methods return mocks by default
mock_user.get_preferences()  # Returns another Mock

Patching with @patch

The @patch decorator replaces objects during a test:

from unittest.mock import patch
 
@patch("myapp.services.requests.get")
@patch("myapp.services.database.get_user")
def test_get_user_weather(mock_get_user, mock_requests_get):
    # Configure mocks
    mock_get_user.return_value = Mock(name="Alice", city="Seattle")
    mock_requests_get.return_value.json.return_value = {"temp": 72}
    
    # Run the actual function
    result = get_user_weather(123)
    
    # Verify behavior
    assert result == "Alice's weather: 72°F"
    mock_get_user.assert_called_once_with(123)
    mock_requests_get.assert_called_once_with("https://api.weather.com/Seattle")

Important: Patch where the object is used, not where it's defined. If myapp/services.py imports requests, patch myapp.services.requests, not requests.

pytest-mock: A Better Interface

The pytest-mock plugin provides a cleaner fixture-based API:

pip install pytest-mock
def test_get_user_weather(mocker):
    # mocker is automatically available as a fixture
    mock_get_user = mocker.patch("myapp.services.database.get_user")
    mock_requests = mocker.patch("myapp.services.requests.get")
    
    mock_get_user.return_value = mocker.Mock(name="Alice", city="Seattle")
    mock_requests.return_value.json.return_value = {"temp": 72}
    
    result = get_user_weather(123)
    
    assert result == "Alice's weather: 72°F"

The mocker fixture automatically cleans up patches after each test.

Mocking Return Values and Side Effects

Control what mocks return:

def test_with_return_value(mocker):
    mock_func = mocker.patch("myapp.calculate")
    
    # Single return value
    mock_func.return_value = 42
    
    # Different values on consecutive calls
    mock_func.side_effect = [1, 2, 3]
    assert mock_func() == 1
    assert mock_func() == 2
    assert mock_func() == 3
    
    # Raise an exception
    mock_func.side_effect = ValueError("Something went wrong")
    
    # Custom logic
    mock_func.side_effect = lambda x: x * 2

Assertions on Mock Calls

Verify your code called dependencies correctly:

def test_call_assertions(mocker):
    mock_send = mocker.patch("myapp.notifications.send_email")
    
    # Run code that should send email
    process_order(order_id=123)
    
    # Verify it was called
    mock_send.assert_called()
    mock_send.assert_called_once()
    mock_send.assert_called_with(
        to="customer@example.com",
        subject="Order Confirmed"
    )
    
    # Check call count
    assert mock_send.call_count == 1
    
    # Inspect call arguments
    args, kwargs = mock_send.call_args
    assert kwargs["to"] == "customer@example.com"

MagicMock for Special Methods

MagicMock supports Python's magic methods:

def test_context_manager(mocker):
    mock_file = mocker.MagicMock()
    mock_file.__enter__.return_value = mock_file
    mock_file.read.return_value = "file contents"
    
    mocker.patch("builtins.open", return_value=mock_file)
    
    with open("test.txt") as f:
        content = f.read()
    
    assert content == "file contents"

Partial Mocking with wraps

Sometimes you want to spy on real objects:

def test_spy_on_method(mocker):
    real_calculator = Calculator()
    
    # Spy on the method - calls real implementation but tracks calls
    spy = mocker.spy(real_calculator, "add")
    
    result = real_calculator.add(2, 3)
    
    assert result == 5  # Real result
    spy.assert_called_once_with(2, 3)  # But we tracked the call

Mocking Properties

Use PropertyMock for properties:

def test_mock_property(mocker):
    mock_user = mocker.Mock()
    type(mock_user).is_admin = mocker.PropertyMock(return_value=True)
    
    assert mock_user.is_admin == True

Mocking Async Functions

For async code, use AsyncMock:

from unittest.mock import AsyncMock
 
async def test_async_function(mocker):
    mock_fetch = mocker.patch(
        "myapp.api.fetch_data",
        new_callable=AsyncMock
    )
    mock_fetch.return_value = {"data": "value"}
    
    result = await process_data()
    
    mock_fetch.assert_awaited_once()

Common Patterns

Mocking datetime

def test_with_frozen_time(mocker):
    mock_datetime = mocker.patch("myapp.utils.datetime")
    mock_datetime.now.return_value = datetime(2026, 3, 22, 12, 0, 0)
    
    assert get_greeting() == "Good afternoon!"

Mocking Environment Variables

def test_with_env_var(mocker):
    mocker.patch.dict("os.environ", {"API_KEY": "test-key"})
    
    client = create_api_client()
    assert client.api_key == "test-key"

Mocking Class Instances

def test_mock_class(mocker):
    MockDatabase = mocker.patch("myapp.Database")
    mock_instance = MockDatabase.return_value
    mock_instance.query.return_value = [{"id": 1}]
    
    results = fetch_users()
    
    assert results == [{"id": 1}]

When Not to Mock

Mocking is powerful but has limits:

  • Don't mock the system under test
  • Don't mock simple data objects—use real ones
  • Don't mock so much that tests pass with broken code
  • Consider integration tests for critical paths

Mock at the boundaries: external APIs, databases, file systems, time. Let your internal logic run for real. That's where the bugs hide.

React to this post: