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) # 1The 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 == 72The 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.
passThis 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") == TrueWhen 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 == TrueThe 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 ValueErrorMixing 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 argumentsUsing 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 AttributeErrorI 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
passMocking 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!
pass3. 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 parameter4. 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 db5. 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
| Need | Use |
|---|---|
| Fake any object | Mock() or MagicMock() |
| Replace during test | @patch("module.name") |
| Replace object attribute | patch.object(obj, "attr") |
| Replace dict entries | patch.dict(d, values) |
| Mock a property | PropertyMock |
| Sequential returns | side_effect=[1, 2, 3] |
| Raise exception | side_effect=Exception() |
| Dynamic return | side_effect=lambda x: x*2 |
| Enforce interface | spec=RealClass |
| Enforce signatures | create_autospec() or autospec=True |
| Mock file operations | mock_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:
- Run fast — mocking external services achieves this
- Are deterministic — mocking time and network achieves this
- 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.