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.

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

Golden rule: Patch where the name is used, not where it is defined. If 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 Mock for 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 check mock.call_count == 0. If you want to verify a specific method was never called: mock.some_method.assert_not_called().