Skip to content
Codeloom
Python

Python Mocking with unittest.mock: Complete Guide

Master Python's unittest.mock library for testing. Learn Mock, MagicMock, patch, side_effect, and spec with real-world examples for unit testing.

·8 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • Use Mock and MagicMock to replace dependencies
  • Patch objects with @patch and context managers
  • Control return values and side effects
  • Verify how mocked objects were called

Prerequisites

  • Python functions and classes
  • Basic understanding of unit testing

Why Mock?

Unit tests should test one thing in isolation. But real code has dependencies: databases, APIs, file systems, the current time, random numbers. Mocking replaces those dependencies with controlled stand-ins so your tests are fast, deterministic, and focused.

Python’s unittest.mock module (part of the standard library since Python 3.3) provides Mock, MagicMock, and patch for exactly this purpose.

Mock and MagicMock Basics

A Mock object accepts any attribute access or method call without raising an error. It records everything that happens to it, so you can assert on it later.

from unittest.mock import Mock, MagicMock

# Create a mock
mock = Mock()

# Call it like a function
mock(1, 2, key='value')

# Access attributes
mock.some_attribute
mock.nested.deep.attribute

# Everything returns another Mock
result = mock.some_method(42)
print(type(result))  # <class 'unittest.mock.Mock'>

# Check how it was called
mock.assert_called_once_with(1, 2, key='value')
mock.some_method.assert_called_with(42)

MagicMock vs Mock

MagicMock is a Mock subclass that pre-implements magic methods (__len__, __iter__, __getitem__, etc.):

from unittest.mock import Mock, MagicMock

# Regular Mock: magic methods raise TypeError
regular = Mock()
# len(regular)  # TypeError: object of type 'Mock' has no len()

# MagicMock: magic methods work
magic = MagicMock()
len(magic)          # Returns 0 (default)
magic[0]            # Returns a Mock
iter(magic)         # Returns an iterator
bool(magic)         # Returns True

# Configure magic method return values
magic.__len__.return_value = 5
print(len(magic))  # 5

magic.__getitem__.return_value = 'item'
print(magic[0])    # 'item'

Use MagicMock by default. Use Mock only when you explicitly want magic methods to fail.

Setting Return Values

from unittest.mock import Mock

# Simple return value
mock_db = Mock()
mock_db.get_user.return_value = {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}

user = mock_db.get_user(1)
print(user['name'])  # 'Alice'

# Nested return values
mock_api = Mock()
mock_api.client.get.return_value.status_code = 200
mock_api.client.get.return_value.json.return_value = {'data': [1, 2, 3]}

response = mock_api.client.get('/api/items')
print(response.status_code)    # 200
print(response.json())         # {'data': [1, 2, 3]}

Side Effects

side_effect lets you control what happens when a mock is called. It can raise exceptions, return different values on successive calls, or run a custom function.

from unittest.mock import Mock

# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database is down")
# mock_db.connect()  # Raises ConnectionError

# Return different values on successive calls
mock_input = Mock()
mock_input.side_effect = ['Alice', '30', 'alice@example.com']
print(mock_input())  # 'Alice'
print(mock_input())  # '30'
print(mock_input())  # 'alice@example.com'

# Custom function as side effect
def fake_lookup(user_id):
    users = {1: 'Alice', 2: 'Bob'}
    if user_id not in users:
        raise KeyError(f"User {user_id} not found")
    return users[user_id]

mock_service = Mock()
mock_service.get_user.side_effect = fake_lookup

print(mock_service.get_user(1))  # 'Alice'
print(mock_service.get_user(2))  # 'Bob'
# mock_service.get_user(99)      # Raises KeyError

Assertions: Verifying Calls

After exercising the code under test, you verify the mock was used correctly:

from unittest.mock import Mock, call

mock = Mock()

# Make some calls
mock.process('a')
mock.process('b')
mock.process('c')

# Was it called at all?
mock.process.assert_called()

# Was it called exactly 3 times?
assert mock.process.call_count == 3

# What was the last call?
mock.process.assert_called_with('c')

# Check all calls in order
mock.process.assert_has_calls([
    call('a'),
    call('b'),
    call('c'),
])

# Check calls regardless of order
mock.process.assert_has_calls([
    call('c'),
    call('a'),
], any_order=True)

# Was it NEVER called with a specific argument?
mock.save = Mock()
mock.save.assert_not_called()

Accessing Call Arguments

from unittest.mock import Mock

mock = Mock()
mock.send_email('alice@test.com', subject='Hello', body='Hi there')

# Get the arguments from the last call
args, kwargs = mock.send_email.call_args

print(args)    # ('alice@test.com',)
print(kwargs)  # {'subject': 'Hello', 'body': 'Hi there'}

# Get all calls
mock.send_email('bob@test.com', subject='Hey')
for c in mock.send_email.call_args_list:
    print(c)

Patching with @patch

patch temporarily replaces an object with a mock during a test. This is the most important tool for isolating code.

The Key Rule: Patch Where It Is Used

You patch the name as it is imported in the module under test, not where it is defined:

# myapp/email_service.py
import smtplib

def send_welcome_email(user_email):
    server = smtplib.SMTP('smtp.example.com', 587)
    server.starttls()
    server.login('app@example.com', 'password')
    server.sendmail('app@example.com', user_email, 'Welcome!')
    server.quit()
    return True
# tests/test_email_service.py
from unittest.mock import patch, MagicMock
from myapp.email_service import send_welcome_email

# Patch smtplib.SMTP in the module where it's USED
@patch('myapp.email_service.smtplib.SMTP')
def test_send_welcome_email(mock_smtp_class):
    # Configure the mock
    mock_server = MagicMock()
    mock_smtp_class.return_value = mock_server

    # Call the function
    result = send_welcome_email('user@test.com')

    # Verify
    assert result is True
    mock_smtp_class.assert_called_once_with('smtp.example.com', 587)
    mock_server.starttls.assert_called_once()
    mock_server.sendmail.assert_called_once_with(
        'app@example.com', 'user@test.com', 'Welcome!'
    )
    mock_server.quit.assert_called_once()

Patching as a Context Manager

from unittest.mock import patch

def test_with_context_manager():
    with patch('myapp.service.requests.get') as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'users': []}

        # Code under test
        from myapp.service import fetch_users
        result = fetch_users()

        assert result == []
        mock_get.assert_called_once()

Patching Multiple Objects

from unittest.mock import patch

@patch('myapp.service.cache.get')
@patch('myapp.service.db.query')
@patch('myapp.service.logger')
def test_get_user(mock_logger, mock_query, mock_cache):
    # Note: decorators are applied bottom-up, so arguments
    # are in reverse order!
    mock_cache.return_value = None  # Cache miss
    mock_query.return_value = {'id': 1, 'name': 'Alice'}

    from myapp.service import get_user
    user = get_user(1)

    assert user['name'] == 'Alice'
    mock_cache.assert_called_once_with('user:1')
    mock_query.assert_called_once_with('SELECT * FROM users WHERE id = ?', (1,))
    mock_logger.info.assert_called()

patch.object

Patch a specific attribute on an object:

from unittest.mock import patch

class PaymentGateway:
    def charge(self, amount, card_token):
        # In real code, this calls an external API
        pass

    def refund(self, transaction_id):
        pass

def test_payment():
    gateway = PaymentGateway()

    with patch.object(gateway, 'charge', return_value={'status': 'success', 'id': 'txn_123'}):
        result = gateway.charge(99.99, 'tok_visa')
        assert result['status'] == 'success'

patch.dict

Temporarily modify a dictionary (commonly used for environment variables):

from unittest.mock import patch
import os

@patch.dict(os.environ, {'API_KEY': 'test-key-123', 'DEBUG': 'true'})
def test_with_env_vars():
    assert os.environ['API_KEY'] == 'test-key-123'
    assert os.environ['DEBUG'] == 'true'

# After the test, os.environ is restored to its original state

@patch.dict(os.environ, {'SECRET': ''}, clear=True)
def test_with_clean_env():
    # os.environ only has 'SECRET', everything else is cleared
    assert 'HOME' not in os.environ

Using spec for Safer Mocks

By default, mocks accept any attribute, which can hide bugs. spec restricts the mock to only allow attributes that exist on the real object:

from unittest.mock import Mock, create_autospec

class UserRepository:
    def get(self, user_id: int) -> dict:
        pass

    def save(self, user: dict) -> None:
        pass

    def delete(self, user_id: int) -> bool:
        pass

# Without spec: typos go unnoticed
bad_mock = Mock()
bad_mock.gett(1)  # No error! But 'gett' is a typo

# With spec: typos raise AttributeError
safe_mock = Mock(spec=UserRepository)
# safe_mock.gett(1)  # AttributeError: Mock object has no attribute 'gett'
safe_mock.get(1)     # Works fine

# create_autospec also checks argument signatures
auto_mock = create_autospec(UserRepository)
auto_mock.get(1)         # Works
# auto_mock.get(1, 2, 3)  # TypeError: too many positional arguments

Always use spec or create_autospec in production test suites. It catches refactoring bugs where a method is renamed but tests still pass because the mock silently accepts the old name.

Real-World Testing Patterns

Testing API Clients

from unittest.mock import patch, Mock
import requests

class WeatherClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = 'https://api.weather.com'

    def get_temperature(self, city):
        response = requests.get(
            f'{self.base_url}/current',
            params={'city': city, 'key': self.api_key}
        )
        response.raise_for_status()
        data = response.json()
        return data['temperature']

@patch('requests.get')
def test_get_temperature(mock_get):
    # Set up the mock response
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        'city': 'London',
        'temperature': 18.5,
        'unit': 'celsius'
    }
    mock_response.raise_for_status = Mock()
    mock_get.return_value = mock_response

    # Test
    client = WeatherClient('fake-key')
    temp = client.get_temperature('London')

    assert temp == 18.5
    mock_get.assert_called_once_with(
        'https://api.weather.com/current',
        params={'city': 'London', 'key': 'fake-key'}
    )

@patch('requests.get')
def test_get_temperature_api_error(mock_get):
    mock_response = Mock()
    mock_response.raise_for_status.side_effect = requests.HTTPError("503 Server Error")
    mock_get.return_value = mock_response

    client = WeatherClient('fake-key')
    try:
        client.get_temperature('London')
        assert False, "Should have raised"
    except requests.HTTPError:
        pass  # Expected

Testing Time-Dependent Code

from unittest.mock import patch
from datetime import datetime

def get_greeting():
    hour = datetime.now().hour
    if hour < 12:
        return "Good morning"
    elif hour < 18:
        return "Good afternoon"
    else:
        return "Good evening"

@patch('__main__.datetime')
def test_morning_greeting(mock_datetime):
    mock_datetime.now.return_value = datetime(2026, 7, 6, 9, 0, 0)
    assert get_greeting() == "Good morning"

@patch('__main__.datetime')
def test_evening_greeting(mock_datetime):
    mock_datetime.now.return_value = datetime(2026, 7, 6, 20, 0, 0)
    assert get_greeting() == "Good evening"

Testing File Operations

from unittest.mock import patch, mock_open

def read_config(path):
    with open(path) as f:
        lines = f.readlines()
    config = {}
    for line in lines:
        line = line.strip()
        if '=' in line and not line.startswith('#'):
            key, value = line.split('=', 1)
            config[key.strip()] = value.strip()
    return config

def test_read_config():
    fake_file_content = """
# Database config
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
"""
    with patch('builtins.open', mock_open(read_data=fake_file_content)):
        config = read_config('/etc/myapp/config')

    assert config['DB_HOST'] == 'localhost'
    assert config['DB_PORT'] == '5432'
    assert config['DB_NAME'] == 'myapp'
    assert '#' not in str(config.keys())  # Comments are excluded

Common Mistakes

Patching the Wrong Location

# myapp/utils.py
from os.path import exists

def check_file(path):
    return exists(path)

# WRONG: patching where it's defined
@patch('os.path.exists')  # This won't work because utils.py already imported it
def test_wrong(mock_exists):
    pass

# RIGHT: patching where it's used
@patch('myapp.utils.exists')  # Patch the name in the module that imported it
def test_right(mock_exists):
    mock_exists.return_value = True
    from myapp.utils import check_file
    assert check_file('/some/path') is True

Over-Mocking

# BAD: mocking everything makes the test meaningless
def test_over_mocked():
    with patch('myapp.calculate') as mock_calc:
        mock_calc.return_value = 42
        assert mock_calc(1, 2) == 42  # You're testing the mock, not your code!

# GOOD: only mock external dependencies
def test_properly_mocked():
    with patch('myapp.service.external_api.fetch') as mock_fetch:
        mock_fetch.return_value = {'rate': 1.5}
        result = calculate_with_live_rate(100)
        assert result == 150.0  # Testing YOUR calculation logic

Wrapping Up

Mocking is essential for writing fast, reliable unit tests. Start with patch to replace external dependencies (APIs, databases, file I/O), use return_value and side_effect to control behavior, and use assertions like assert_called_once_with to verify interactions. Always use spec or create_autospec to catch attribute errors early. The most common mistake is patching the wrong location — remember to patch where an object is used, not where it is defined.