When I first started writing tests, I kept hitting the same wall: how do I test code that calls an external API? Or reads from a database? Or checks the current time? The answer is mocking, and Python's unittest.mock module is surprisingly powerful once you understand it.

This is everything I've learned about mocking—the concepts that confused me, the patterns that clicked, and the mistakes I made so you don't have to.

Why Mock at All?

Consider this function:

import requests
from datetime import datetime
 
def get_user_status(user_id):
    response = requests.get(f"https://api.company.com/users/{user_id}")
    user = response.json()
    
    if user["last_active"]:
        last_active = datetime.fromisoformat(user["last_active"])
        days_inactive = (datetime.now() - last_active).days
        if days_inactive > 30:
            return "inactive"
    
    return "active"

Testing this directly would require:

  • A working network connection
  • A real API that exists and responds correctly
  • The current time to behave predictably

That's fragile. Tests would fail when the network is down, when the API changes, or when running at different times. Mocking lets us replace these external dependencies with controlled substitutes.

Mock and MagicMock Basics

The Mock class creates objects that accept any attribute access or method call:

from unittest.mock import Mock
 
# Create a mock
mock = Mock()
 
# Call it like any function
result = mock(1, 2, 3)
 
# Access any attribute
print(mock.foo)           # <Mock name='mock.foo'>
print(mock.foo.bar.baz)   # <Mock name='mock.foo.bar.baz'>
 
# Call any method
mock.do_something("arg")
 
# It remembers everything
print(mock.call_args)      # call(1, 2, 3)
print(mock.call_count)     # 1

The key insight: Mock objects are infinitely flexible. They return new Mock objects for any attribute or method you access. This is both powerful and dangerous—typos won't raise errors.

Configuring Return Values

from unittest.mock import Mock
 
# Set a return value
mock = Mock(return_value=42)
print(mock())  # 42
 
# Or configure after creation
mock = Mock()
mock.calculate.return_value = 100
print(mock.calculate())  # 100
 
# Chain returns for nested calls
mock.get_user.return_value.name = "Alice"
print(mock.get_user().name)  # "Alice"

MagicMock: Mock with Magic Methods

MagicMock is Mock with Python's magic methods pre-configured:

from unittest.mock import Mock, MagicMock
 
# Regular Mock doesn't support magic methods by default
mock = Mock()
# len(mock)  # TypeError: object of type 'Mock' has no len()
 
# MagicMock does
magic = MagicMock()
print(len(magic))      # 0 (default)
print(magic[0])        # MagicMock
print(str(magic))      # '<MagicMock ...>'
 
# Configure magic methods
magic.__len__.return_value = 5
magic.__getitem__.return_value = "item"
print(len(magic))      # 5
print(magic["key"])    # "item"
 
# Useful for context managers
mock_file = MagicMock()
mock_file.__enter__.return_value = mock_file
mock_file.read.return_value = "file contents"
 
with mock_file as f:
    print(f.read())  # "file contents"

In practice, I use MagicMock most of the time since it's more flexible.

patch(): The Star of the Show

Mock creates fake objects, but patch() is how you swap real objects for fakes during tests. It works as a decorator or context manager.

As a Decorator

from unittest.mock import patch
 
# myapp/weather.py
import requests
 
def get_temperature(city):
    response = requests.get(f"https://api.weather.com/{city}")
    return response.json()["temp"]
 
# tests/test_weather.py
@patch("myapp.weather.requests.get")
def test_get_temperature(mock_get):
    # Configure the mock
    mock_response = Mock()
    mock_response.json.return_value = {"temp": 72}
    mock_get.return_value = mock_response
    
    # Call the real function—but requests.get is mocked
    temp = get_temperature("Seattle")
    
    # Verify
    assert temp == 72
    mock_get.assert_called_once_with("https://api.weather.com/Seattle")

As a Context Manager

from unittest.mock import patch
 
def test_get_temperature():
    with patch("myapp.weather.requests.get") as mock_get:
        mock_response = Mock()
        mock_response.json.return_value = {"temp": 72}
        mock_get.return_value = mock_response
        
        temp = get_temperature("Seattle")
        assert temp == 72

The context manager form is great when you only need to mock part of a test.

Multiple Patches

Stack decorators for multiple patches. Important: they apply bottom-up, but arguments come left-to-right:

@patch("myapp.module.func_c")  # Applied third, arg third
@patch("myapp.module.func_b")  # Applied second, arg second  
@patch("myapp.module.func_a")  # Applied first, arg first
def test_multiple(mock_a, mock_b, mock_c):
    # mock_a patches func_a, mock_b patches func_b, etc.
    pass

This confused me for weeks. Just remember: read the decorators bottom-to-top to match the argument order.

Where to Patch (This Tripped Me Up)

The cardinal rule: patch where the object is used, not where it's defined.

# myapp/utils.py
from os.path import exists
 
def check_file(path):
    return exists(path)
 
# tests/test_utils.py
 
# WRONG: patches the original location
@patch("os.path.exists")
def test_wrong(mock_exists):
    mock_exists.return_value = True
    # check_file still uses the original exists!
    
# RIGHT: patch where it was imported
@patch("myapp.utils.exists")
def test_right(mock_exists):
    mock_exists.return_value = True
    assert check_file("/any/path") == True

When myapp.utils imports exists, it creates a reference in its own namespace. Patching os.path.exists doesn't affect that reference—you need to patch myapp.utils.exists.

patch.object for Specific Attributes

When you need to patch a method on a specific object or class:

from unittest.mock import patch
 
class EmailService:
    def send(self, to, subject, body):
        # Actually sends email
        pass
 
# Patch the method on the class
with patch.object(EmailService, "send", return_value=True) as mock_send:
    service = EmailService()
    result = service.send("test@example.com", "Hi", "Hello!")
    
    assert result == True
    mock_send.assert_called_once()
 
# Patch on a specific instance
service = EmailService()
with patch.object(service, "send", return_value=True):
    service.send("test@example.com", "Hi", "Hello!")

This is cleaner than patching the full module path when you already have a reference to the object.

PropertyMock for Mocking Properties

Properties require special handling because they're descriptors:

from unittest.mock import patch, PropertyMock
 
class Config:
    @property
    def api_url(self):
        return self._load_from_env()  # Expensive operation
    
    @property
    def is_production(self):
        return self.api_url.startswith("https://api.prod")
 
# Test with mocked property
def test_is_production():
    with patch.object(
        Config, 
        "api_url", 
        new_callable=PropertyMock,
        return_value="https://api.prod.example.com"
    ):
        config = Config()
        assert config.is_production == True

The new_callable=PropertyMock is essential. Without it, accessing config.api_url would return the Mock object itself, not the return value.

Mocking Properties on Instances

from unittest.mock import PropertyMock, patch
 
class User:
    @property
    def full_name(self):
        return f"{self.first} {self.last}"
 
user = User()
 
# Patch on the class, not the instance
with patch.object(User, "full_name", new_callable=PropertyMock) as mock_name:
    mock_name.return_value = "Test User"
    print(user.full_name)  # "Test User"

side_effect: Dynamic Behavior

side_effect is incredibly versatile. Use it for:

Raising Exceptions

from unittest.mock import Mock
import pytest
 
mock = Mock()
mock.side_effect = ValueError("Something went wrong")
 
with pytest.raises(ValueError, match="Something went wrong"):
    mock()

Different Returns on Consecutive Calls

from unittest.mock import Mock
 
mock = Mock()
mock.side_effect = [1, 2, 3]
 
print(mock())  # 1
print(mock())  # 2
print(mock())  # 3
# print(mock())  # StopIteration!

Dynamic Return Based on Input

from unittest.mock import Mock
 
def mock_get_user(user_id):
    users = {
        1: {"name": "Alice", "role": "admin"},
        2: {"name": "Bob", "role": "user"},
    }
    if user_id not in users:
        raise ValueError(f"User {user_id} not found")
    return users[user_id]
 
mock = Mock(side_effect=mock_get_user)
 
print(mock(1))  # {"name": "Alice", "role": "admin"}
print(mock(2))  # {"name": "Bob", "role": "user"}
# mock(999)     # Raises ValueError

Mixing Exceptions and Returns

from unittest.mock import Mock
 
mock = Mock()
mock.side_effect = [
    {"status": "ok"},
    ConnectionError("Network down"),
    {"status": "ok"},  # Retry succeeded
]
 
print(mock())  # {"status": "ok"}
# mock()       # Raises ConnectionError
# mock()       # {"status": "ok"}

spec and autospec: Safer Mocking

Plain Mock objects accept any attribute or method call. This means typos don't cause errors:

from unittest.mock import Mock
 
class Database:
    def query(self, sql):
        pass
 
mock_db = Mock()
mock_db.qeury("SELECT *")  # Typo! But no error...

spec: Match the Interface

from unittest.mock import Mock
 
mock_db = Mock(spec=Database)
mock_db.query("SELECT *")    # Works
# mock_db.qeury("SELECT *")  # AttributeError!
# mock_db.nonexistent()      # AttributeError!

autospec: Match Signatures Too

from unittest.mock import create_autospec
 
class Calculator:
    def add(self, a: int, b: int) -> int:
        return a + b
 
mock_calc = create_autospec(Calculator)
mock_calc.add(1, 2)           # Works
# mock_calc.add(1)            # TypeError: missing argument 'b'
# mock_calc.add(1, 2, 3)      # TypeError: too many arguments

Using autospec with patch

from unittest.mock import patch
 
@patch("myapp.services.Database", autospec=True)
def test_with_autospec(MockDatabase):
    mock_instance = MockDatabase.return_value
    mock_instance.query.return_value = [{"id": 1}]
    
    # Your test code
    # Any typos in method names will raise AttributeError

I now use autospec=True by default. It catches so many bugs.

Common Patterns

Mocking External APIs

from unittest.mock import patch, Mock
import pytest
 
def fetch_user(user_id):
    import requests
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()
 
@patch("requests.get")
def test_fetch_user_success(mock_get):
    # Configure mock response
    mock_response = Mock()
    mock_response.json.return_value = {"id": 1, "name": "Alice"}
    mock_response.raise_for_status = Mock()  # No exception
    mock_get.return_value = mock_response
    
    user = fetch_user(1)
    
    assert user["name"] == "Alice"
    mock_get.assert_called_once_with("https://api.example.com/users/1")
 
@patch("requests.get")
def test_fetch_user_not_found(mock_get):
    import requests
    
    mock_response = Mock()
    mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
    mock_get.return_value = mock_response
    
    with pytest.raises(requests.HTTPError):
        fetch_user(999)
 
@patch("requests.get")
def test_fetch_user_network_error(mock_get):
    import requests
    
    mock_get.side_effect = requests.ConnectionError("Network unreachable")
    
    with pytest.raises(requests.ConnectionError):
        fetch_user(1)

Mocking File Operations

from unittest.mock import patch, mock_open, MagicMock
 
def read_config(path):
    with open(path) as f:
        return f.read()
 
def test_read_config():
    mock_data = "api_key=secret123\ndebug=true"
    
    with patch("builtins.open", mock_open(read_data=mock_data)):
        config = read_config("/etc/myapp/config")
        
        assert "api_key=secret123" in config
        assert "debug=true" in config
 
# Writing files
def write_log(path, message):
    with open(path, "a") as f:
        f.write(message + "\n")
 
def test_write_log():
    m = mock_open()
    
    with patch("builtins.open", m):
        write_log("/var/log/app.log", "Test message")
        
        m.assert_called_once_with("/var/log/app.log", "a")
        handle = m()
        handle.write.assert_called_once_with("Test message\n")
 
# Reading multiple files
def test_multiple_files():
    files = {
        "/config/db.yml": "host: localhost",
        "/config/app.yml": "debug: true",
    }
    
    def mock_file_open(path, *args, **kwargs):
        if path in files:
            return mock_open(read_data=files[path])()
        raise FileNotFoundError(path)
    
    with patch("builtins.open", side_effect=mock_file_open):
        # Your test code
        pass

Mocking datetime

This is a classic problem—how do you test time-dependent code?

from unittest.mock import patch, Mock
from datetime import datetime, timedelta
 
def get_greeting():
    hour = datetime.now().hour
    if hour < 12:
        return "Good morning"
    elif hour < 17:
        return "Good afternoon"
    else:
        return "Good evening"
 
def test_get_greeting_morning():
    mock_datetime = Mock()
    mock_datetime.now.return_value = datetime(2026, 3, 22, 9, 0, 0)
    
    with patch("__main__.datetime", mock_datetime):
        # Note: patch the module where datetime is used
        assert get_greeting() == "Good morning"
 
def test_get_greeting_afternoon():
    mock_datetime = Mock()
    mock_datetime.now.return_value = datetime(2026, 3, 22, 14, 0, 0)
    
    with patch("__main__.datetime", mock_datetime):
        assert get_greeting() == "Good afternoon"
 
# Alternative: use freezegun library (pip install freezegun)
from freezegun import freeze_time
 
@freeze_time("2026-03-22 09:00:00")
def test_greeting_morning_freezegun():
    assert get_greeting() == "Good morning"

Mocking Environment Variables

from unittest.mock import patch
import os
 
def get_database_url():
    return os.environ.get("DATABASE_URL", "sqlite:///default.db")
 
def test_get_database_url_from_env():
    with patch.dict(os.environ, {"DATABASE_URL": "postgres://localhost/test"}):
        assert get_database_url() == "postgres://localhost/test"
 
def test_get_database_url_default():
    with patch.dict(os.environ, {}, clear=True):
        # clear=True removes all existing env vars
        assert get_database_url() == "sqlite:///default.db"

Mocking Class Instantiation

from unittest.mock import patch, Mock
 
class DatabaseConnection:
    def __init__(self, host, port):
        self.connection = self._connect(host, port)
    
    def _connect(self, host, port):
        # Actually connects to database
        pass
    
    def query(self, sql):
        pass
 
def get_users():
    db = DatabaseConnection("localhost", 5432)
    return db.query("SELECT * FROM users")
 
@patch("myapp.DatabaseConnection")
def test_get_users(MockDatabaseConnection):
    # Configure the mock instance
    mock_instance = Mock()
    mock_instance.query.return_value = [{"id": 1, "name": "Alice"}]
    MockDatabaseConnection.return_value = mock_instance
    
    users = get_users()
    
    assert users == [{"id": 1, "name": "Alice"}]
    MockDatabaseConnection.assert_called_once_with("localhost", 5432)
    mock_instance.query.assert_called_once_with("SELECT * FROM users")

Testing Best Practices

After writing hundreds of tests with mocks, here's what I've learned:

1. Use spec/autospec by Default

# Instead of this
mock = Mock()
 
# Do this
mock = Mock(spec=RealClass)
 
# Or with patch
@patch("myapp.module.SomeClass", autospec=True)

This catches typos and API drift immediately.

2. Patch at the Boundary

Mock external dependencies (APIs, databases, file systems, time), not your own code:

# Good: mock the external service
@patch("myapp.services.requests.get")
def test_fetch_user(mock_get):
    pass
 
# Bad: mock your own function
@patch("myapp.services.process_user")
def test_something(mock_process):
    # You're not testing process_user anymore!
    pass

3. Verify Behavior, Not Implementation

# Good: verify the important behavior
mock_email.assert_called()
assert "welcome" in mock_email.call_args.kwargs["subject"].lower()
 
# Risky: over-specifying
mock_email.assert_called_once_with(
    to="user@example.com",
    subject="Welcome to Our Platform!",
    body="Dear User,\n\nWelcome...",
    html=True,
    priority="normal",
    # ... every single argument
)
# This breaks if you add a harmless parameter

4. Keep Mocks Simple

# If your mock setup is this complex...
mock_db = Mock()
mock_db.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [...]
 
# Consider restructuring your code or using fixtures
@pytest.fixture
def mock_db():
    db = Mock(spec=Database)
    db.get_users.return_value = [User(id=1, name="Alice")]
    return db

5. Don't Mock What You Don't Own (Sometimes)

For complex third-party libraries, consider using their testing utilities:

# Instead of mocking requests deeply
# Use responses library (pip install responses)
import responses
 
@responses.activate
def test_api_call():
    responses.add(
        responses.GET,
        "https://api.example.com/users/1",
        json={"id": 1, "name": "Alice"},
        status=200,
    )
    
    user = fetch_user(1)
    assert user["name"] == "Alice"

6. Name Your Mocks

# Harder to debug
mock = Mock()
 
# Better: meaningful names
mock_user_service = Mock(name="UserService")
mock_user_service.get_user(1)
# Error messages now say "UserService" not "Mock"

Quick Reference

NeedUse
Fake any objectMock() or MagicMock()
Replace during test@patch("module.name")
Replace object attributepatch.object(obj, "attr")
Replace dict entriespatch.dict(d, values)
Mock a propertyPropertyMock
Sequential returnsside_effect=[1, 2, 3]
Raise exceptionside_effect=Exception()
Dynamic returnside_effect=lambda x: x*2
Enforce interfacespec=RealClass
Enforce signaturescreate_autospec() or autospec=True
Mock file operationsmock_open(read_data="...")
Verify called.assert_called()
Verify arguments.assert_called_with(...)
Check call count.call_count
Check all calls.call_args_list
Reset mock.reset_mock()

Final Thoughts

Mocking is a tool, not a goal. The best tests are those that:

  1. Run fast — mocking external services achieves this
  2. Are deterministic — mocking time and network achieves this
  3. Test real behavior — don't mock so much that you're testing mocks

Start with Mock and @patch. Add spec and autospec as you get comfortable. Use side_effect for complex scenarios. And when your mock setup gets unwieldy, it's often a sign your code could be refactored.

Happy testing.

React to this post: