Skip to main content

Mocking

In this chapter, we'll learn about mocking - a powerful testing technique. We'll cover:

  • What is mocking and when to use it
  • Python's unittest.mock module
  • Mock, MagicMock, and patch
  • Using pytest-mock for cleaner syntax
  • Best practices for mocking
  • Testing previously untested code with mocks

What is mocking?

Mocking replaces real objects with controlled substitutes during testing. This is useful when:

  • Real dependencies are slow (databases, APIs)
  • Real dependencies have side effects (sending emails, charging credit cards)
  • You want to test error conditions that are hard to trigger
  • You want to isolate the code under test

Python's unittest.mock

The unittest.mock module is part of Python's standard library.

Basic Mock usage

from unittest.mock import Mock

# Create a mock object
mock = Mock()

# Call it like a function
result = mock(1, 2, 3)

# Access attributes
mock.some_attribute

# Mock returns another Mock by default
assert isinstance(mock(), Mock)
assert isinstance(mock.method(), Mock)

Configuring return values

from unittest.mock import Mock

mock = Mock()
mock.return_value = 42

assert mock() == 42
assert mock("ignored", "arguments") == 42

Setting up method return values

mock = Mock()
mock.get_name.return_value = "Alice"
mock.calculate.return_value = 100

assert mock.get_name() == "Alice"
assert mock.calculate() == 100

Testing with Mock

Write the test first

Let's test a user service that fetches data from a database:

from unittest.mock import Mock
from user_service import UserService


def test_get_user_full_name():
# Create a mock database
mock_db = Mock()
mock_db.find_user.return_value = {"first": "Alice", "last": "Smith"}

# Inject the mock
service = UserService(mock_db)

# Test the service
result = service.get_full_name(user_id=123)

assert result == "Alice Smith"
mock_db.find_user.assert_called_once_with(123)

Implementation

class UserService:
def __init__(self, database):
self.db = database

def get_full_name(self, user_id):
user = self.db.find_user(user_id)
return f"{user['first']} {user['last']}"

Assertions on mocks

Mocks record how they were called:

from unittest.mock import Mock

mock = Mock()
mock(1, 2, 3, key="value")

# Verify the call
mock.assert_called()
mock.assert_called_once()
mock.assert_called_with(1, 2, 3, key="value")
mock.assert_called_once_with(1, 2, 3, key="value")

# Check call history
assert mock.call_count == 1
assert mock.call_args == ((1, 2, 3), {"key": "value"})

Testing call sequences

from unittest.mock import Mock, call

mock = Mock()
mock.add(1)
mock.add(2)
mock.add(3)

# Verify sequence of calls
mock.add.assert_has_calls([call(1), call(2), call(3)])

MagicMock

MagicMock is a subclass of Mock that implements magic methods:

from unittest.mock import MagicMock

# MagicMock supports len(), iter(), etc.
mock = MagicMock()
mock.__len__.return_value = 5

assert len(mock) == 5

Using MagicMock as a context manager

mock = MagicMock()
mock.__enter__.return_value = "entered"

with mock as value:
assert value == "entered"

The patch decorator

patch temporarily replaces an object with a mock:

from unittest.mock import patch

# As a decorator
@patch('module.ClassName')
def test_something(MockClass):
instance = MockClass.return_value
instance.method.return_value = 42

# Your test code here

# As a context manager
def test_something_else():
with patch('module.ClassName') as MockClass:
instance = MockClass.return_value
instance.method.return_value = 42

# Your test code here

Patching where it's used

Important: Patch where the object is used, not where it's defined:

# user_service.py
from database import Database

class UserService:
def __init__(self):
self.db = Database() # Creates Database instance

# test_user_service.py
# Patch where Database is used (in user_service), not where it's defined
@patch('user_service.Database')
def test_user_service(MockDatabase):
mock_db = MockDatabase.return_value
mock_db.find_user.return_value = {"name": "Alice"}

service = UserService()
# ...

pytest-mock

The pytest-mock plugin provides a cleaner API:

# Install: pip install pytest-mock

def test_with_mocker(mocker):
# mocker.patch is like unittest.mock.patch
mock_db = mocker.patch('user_service.Database')
mock_db.return_value.find_user.return_value = {"name": "Alice"}

service = UserService()
# ...

spy()

Spy on a real object:

def test_spy(mocker):
calculator = Calculator()

# Spy wraps the real method
spy = mocker.spy(calculator, 'add')

result = calculator.add(1, 2)

assert result == 3 # Real result
spy.assert_called_once_with(1, 2) # Verify call

Testing exceptions

Configure mocks to raise exceptions:

from unittest.mock import Mock


def test_handles_database_error():
mock_db = Mock()
mock_db.find_user.side_effect = ConnectionError("Database unavailable")

service = UserService(mock_db)

with pytest.raises(ServiceError, match="Could not fetch user"):
service.get_full_name(123)

Multiple side effects

mock = Mock()

# Return different values on successive calls
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3

# Or call a function
mock.side_effect = lambda x: x * 2
assert mock(5) == 10

Testing previously untested code

When adding tests to existing code, mocking helps isolate dependencies.

Example: Untested email sender

# email_sender.py (existing, untested code)
import smtplib

def send_welcome_email(user_email, username):
server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()
server.login('app@example.com', 'password')

message = f"Welcome {username}!"
server.sendmail('app@example.com', user_email, message)
server.quit()

return True

Adding tests with patch

from unittest.mock import patch, MagicMock
from email_sender import send_welcome_email


@patch('email_sender.smtplib.SMTP')
def test_send_welcome_email(MockSMTP):
mock_server = MagicMock()
MockSMTP.return_value = mock_server

result = send_welcome_email('alice@example.com', 'Alice')

assert result is True
MockSMTP.assert_called_once_with('smtp.example.com', 587)
mock_server.starttls.assert_called_once()
mock_server.sendmail.assert_called_once_with(
'app@example.com',
'alice@example.com',
'Welcome Alice!'
)
mock_server.quit.assert_called_once()

When to mock and when not to

Good use cases

  • External APIs and network calls
  • Databases when testing business logic
  • File system operations
  • Time-dependent operations
  • Expensive computations

Avoid mocking

  • Simple data structures
  • Pure functions
  • Code you own and control (prefer real implementations)
  • Too many internals (brittle tests)

Best practices

1. Mock at the boundary

Mock external systems, not internal classes:

# Good: Mock the external API
@patch('weather_service.requests.get')
def test_weather_service(mock_get):
mock_get.return_value.json.return_value = {"temp": 20}

service = WeatherService()
assert service.get_temperature("London") == 20

# Bad: Mock internal helpers
@patch('weather_service.parse_response')
def test_weather_service(mock_parse): # Don't do this
pass

2. Prefer dependency injection

# Good: Inject dependency
class WeatherService:
def __init__(self, api_client):
self.api = api_client

# Test
def test_weather_service():
fake_api = FakeWeatherAPI()
service = WeatherService(fake_api)

# Less good: Patch global dependency
@patch('weather_service.requests')
def test_weather_service(mock_requests):
pass

3. Keep mocks simple

# Good: Simple, focused mock
mock_db = Mock()
mock_db.find_user.return_value = {"name": "Alice"}

# Bad: Overly complex mock
mock_db = Mock()
mock_db.connect.return_value = True
mock_db.cursor.return_value.execute.return_value = None
mock_db.cursor.return_value.fetchone.return_value = ("Alice",)

4. Test behavior, not implementation

# Good: Test what the code does
def test_sends_notification():
mock_sender = Mock()
service = NotificationService(mock_sender)

service.notify_user(123, "Hello")

mock_sender.send.assert_called()

# Bad: Test how the code works
def test_notification_implementation():
mock_sender = Mock()
service = NotificationService(mock_sender)

service.notify_user(123, "Hello")

# Too specific about internal details
assert service._prepare_message_called
assert service._format_recipient_called

Complete example: Order service

# test_order_service.py
from unittest.mock import Mock, patch
import pytest
from order_service import OrderService


class TestOrderService:
def test_place_order_success(self):
mock_inventory = Mock()
mock_inventory.check_availability.return_value = True
mock_inventory.reserve.return_value = True

mock_payment = Mock()
mock_payment.charge.return_value = {"status": "success"}

service = OrderService(
inventory=mock_inventory,
payment=mock_payment
)

order = service.place_order(
item_id="ABC123",
quantity=2,
card_token="tok_visa"
)

assert order["status"] == "confirmed"
mock_inventory.reserve.assert_called_once_with("ABC123", 2)
mock_payment.charge.assert_called_once()

def test_place_order_out_of_stock(self):
mock_inventory = Mock()
mock_inventory.check_availability.return_value = False

mock_payment = Mock()

service = OrderService(
inventory=mock_inventory,
payment=mock_payment
)

with pytest.raises(Exception, match="Out of stock"):
service.place_order("ABC123", 2, "tok_visa")

# Payment should never be called
mock_payment.charge.assert_not_called()

Wrapping up

We've covered:

  • Mock - Create mock objects with configurable behavior
  • MagicMock - Mock with magic method support
  • patch - Temporarily replace objects during tests
  • pytest-mock - Cleaner mocking with pytest
  • side_effect - Configure exceptions or multiple return values
  • Assertions - Verify how mocks were called

Key takeaways

  1. Mock external dependencies, not internal code
  2. Prefer dependency injection over patching
  3. Keep mocks simple and focused
  4. Test behavior, not implementation details
  5. Use mocking to test error conditions

Mocking is a powerful tool, but use it wisely. The goal is testable, maintainable code - not maximum mock coverage!