Python Mock and Patch: Unit Testing External Dependencies
Unit tests should run fast and in isolation — without real databases, HTTP calls, or file system access. Python's unittest.mock provides MagicMock, patch, and AsyncMock to replace external dependencies with controllable fakes. This guide covers the essential mocking patterns: patching at the right location, controlling return values and side effects, verifying calls, mocking async code, and common mistakes to avoid.
Table of Contents
Mock and MagicMock Basics
from unittest.mock import Mock, MagicMock, call
# Mock: auto-creates attributes and methods
m = Mock()
m.some_method()
m.some_method.assert_called_once()
# Configure return values
m.get_user.return_value = {"id": 1, "name": "Alice"}
result = m.get_user(1)
assert result["name"] == "Alice"
# MagicMock: supports magic methods (__len__, __iter__, __str__, etc.)
mm = MagicMock()
mm.__len__.return_value = 5
mm.__iter__.return_value = iter([1, 2, 3])
assert len(mm) == 5
assert list(mm) == [1, 2, 3]
# Verify calls
service = Mock()
service.process(data={"key": "value"}, retry=True)
service.process.assert_called_once_with(data={"key": "value"}, retry=True)
# call_count, call_args_list
service.notify("a")
service.notify("b")
service.notify("c")
assert service.notify.call_count == 3
assert service.notify.call_args_list == [call("a"), call("b"), call("c")]
# assert_any_call — order doesn't matter
service.notify.assert_any_call("b")
@patch Decorator
@patch replaces an object in the test namespace for the duration of the test. It injects the mock as an argument to your test function — last applied patch is the first argument.
from unittest.mock import patch, MagicMock
import pytest
# Patch by dotted path
@patch("myapp.services.requests.get")
def test_fetch_user(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
from myapp.services import fetch_user
result = fetch_user(1)
mock_get.assert_called_once_with("https://api.example.com/users/1")
assert result["name"] == "Alice"
# Multiple patches — order is bottom-up in args
@patch("myapp.email.send_email")
@patch("myapp.db.get_user")
def test_notify_user(mock_get_user, mock_send_email): # db first, email second
mock_get_user.return_value = {"id": 1, "email": "alice@example.com"}
from myapp import notify_user
notify_user(1)
mock_get_user.assert_called_once_with(1)
mock_send_email.assert_called_once_with(
to="alice@example.com",
subject="Notification",
body=mock_send_email.call_args[1]["body"],
)
# Context manager style
def test_something():
with patch("myapp.config.DEBUG", True):
# DEBUG is True only in this block
from myapp import do_something
assert do_something() == "debug mode output"
# patch.object — patch a method on an existing instance
class EmailSender:
def send(self, to, body): ...
def test_email():
sender = EmailSender()
with patch.object(sender, "send", return_value=None) as mock_send:
sender.send("test@example.com", "hello")
mock_send.assert_called_once()
Patching at the Right Location
myapp.services does from requests import get, patch myapp.services.get — not requests.get.
# WRONG — patches the original module, not the imported name
@patch("requests.get") # too late — services.py already imported it
def test_wrong(mock_get):
from myapp.services import fetch_data
fetch_data() # mock_get never called!
# CORRECT — patch where it's used
@patch("myapp.services.get") # patches the name 'get' in myapp.services
def test_correct(mock_get):
from myapp.services import fetch_data
fetch_data() # mock_get IS called
# If services.py does 'import requests' (not 'from requests import get'):
@patch("myapp.services.requests")
def test_module_import(mock_requests):
mock_requests.get.return_value.json.return_value = {"key": "value"}
from myapp.services import fetch_data
fetch_data() # works
Return Values and Side Effects
from unittest.mock import MagicMock
m = MagicMock()
# Different return value each call
m.next_id.side_effect = [1, 2, 3, 4]
print(m.next_id()) # 1
print(m.next_id()) # 2
print(m.next_id()) # 3
# Raise an exception
m.connect.side_effect = ConnectionError("Database unavailable")
try:
m.connect()
except ConnectionError as e:
print(f"Caught: {e}")
# Mix returns and exceptions
m.retry.side_effect = [
ConnectionError("First attempt failed"),
ConnectionError("Second attempt failed"),
{"status": "ok"}, # third attempt succeeds
]
# side_effect as a callable
def dynamic_response(user_id):
if user_id == 99:
raise ValueError("Invalid user ID")
return {"id": user_id, "name": f"User {user_id}"}
m.get_user.side_effect = dynamic_response
print(m.get_user(1)) # {"id": 1, "name": "User 1"}
print(m.get_user(5)) # {"id": 5, "name": "User 5"}
# m.get_user(99) # raises ValueError
AsyncMock for Async Code
from unittest.mock import AsyncMock, patch
import pytest
import asyncio
# AsyncMock — coroutine that returns a value
async def test_async_service():
mock_db = AsyncMock()
mock_db.fetch_user.return_value = {"id": 1, "name": "Alice"}
from myapp.services import UserService
svc = UserService(db=mock_db)
user = await svc.get_profile(1)
mock_db.fetch_user.assert_awaited_once_with(1)
assert user["name"] == "Alice"
# Patch an async function
@pytest.mark.asyncio
@patch("myapp.services.fetch_from_api", new_callable=AsyncMock)
async def test_api_call(mock_fetch):
mock_fetch.return_value = {"results": [1, 2, 3]}
from myapp.services import get_data
result = await get_data()
mock_fetch.assert_awaited_once()
assert result == {"results": [1, 2, 3]}
# AsyncMock with side_effect
@pytest.mark.asyncio
async def test_retry_logic():
mock_client = AsyncMock()
mock_client.get.side_effect = [
Exception("timeout"),
Exception("timeout"),
{"data": "success"},
]
from myapp.client import resilient_get
result = await resilient_get(mock_client, "/endpoint")
assert result == {"data": "success"}
assert mock_client.get.await_count == 3
spec and autospec
By default, Mock accepts any attribute or method call — including typos. Use spec to constrain the mock to only allow attributes that exist on the real class.
from unittest.mock import MagicMock, create_autospec, patch
class UserRepository:
async def find_by_id(self, user_id: int) -> dict: ...
async def save(self, user: dict) -> dict: ...
# Without spec — typos silently pass
m = MagicMock()
m.fnd_by_id(1) # typo! No error raised
# With spec — enforces the interface
m = MagicMock(spec=UserRepository)
# m.fnd_by_id(1) # AttributeError! Catches the typo
m.find_by_id.return_value = {"id": 1} # correct name
# autospec — also validates call signatures
m = create_autospec(UserRepository)
m.find_by_id(1) # OK
# m.find_by_id(1, 2) # TypeError: too many arguments
# m.find_by_id("one") # (type checking depends on annotations)
# patch with autospec
@patch("myapp.services.UserRepository", autospec=True)
def test_service(MockRepo):
instance = MockRepo.return_value
instance.find_by_id.return_value = {"id": 1}
# ...
Common Mocking Patterns
from unittest.mock import patch, MagicMock, mock_open
import pytest
# Mock file I/O
def read_config(path: str) -> dict:
import json
with open(path) as f:
return json.load(f)
def test_read_config():
fake_content = '{"debug": true, "port": 8080}'
with patch("builtins.open", mock_open(read_data=fake_content)):
config = read_config("/etc/app/config.json")
assert config["port"] == 8080
# Mock datetime.now()
from datetime import datetime
@patch("myapp.services.datetime")
def test_with_fixed_time(mock_dt):
mock_dt.now.return_value = datetime(2026, 6, 14, 12, 0, 0)
mock_dt.utcnow.return_value = datetime(2026, 6, 14, 12, 0, 0)
from myapp.services import create_event
event = create_event("meeting")
assert event["created_at"] == "2026-06-14T12:00:00"
# Mock environment variables
@patch.dict("os.environ", {"DATABASE_URL": "sqlite:///:memory:", "DEBUG": "true"})
def test_with_env():
import os
assert os.environ["DATABASE_URL"] == "sqlite:///:memory:"
# Mock requests.Session
@patch("myapp.client.requests.Session")
def test_http_client(MockSession):
mock_session = MockSession.return_value.__enter__.return_value
mock_session.get.return_value.json.return_value = {"status": "ok"}
mock_session.get.return_value.raise_for_status.return_value = None
from myapp.client import ApiClient
client = ApiClient("https://api.example.com")
result = client.get_status()
assert result["status"] == "ok"
Frequently Asked Questions
- When should I use Mock vs a real fake class?
- Use
Mockfor simple, one-off replacements where you just need to control return values and verify calls. Use a real fake class (hand-written implementation) when the mock logic is complex, when you reuse it across many tests, or when the real behaviour matters for the test correctness. - Why does my patch not work?
- You're almost certainly patching the wrong location. Remember: patch where the name is used, not where it's defined. Use
import myapp.services; print(dir(myapp.services))to confirm the name to patch. - How do I assert a mock was NOT called?
- Use
mock.assert_not_called(). Or checkmock.call_count == 0. If you want to verify a specific method was never called:mock.some_method.assert_not_called().