Mocking and Patching

Chapter 4: Mocking and Patching

Testing code that depends on external systems—databases, APIs, file systems, time—is challenging. You can't hit a real API in unit tests; it's slow, unreliable, and might cost money. This is where mocking comes in. Mocking lets you replace real dependencies with test doubles that behave exactly how you need them to for testing. This chapter teaches you how to use Python's unittest.mock module to test code with external dependencies.

What is Mocking?

Mocking replaces real objects with fake ones during testing. Instead of calling a real database, your test uses a mock that returns predefined data. Instead of sending actual emails, your code calls a mock that records what would have been sent. This isolation makes tests fast, reliable, and deterministic.

Test Doubles is the general term for fake objects in testing. There are several types:

Mocks: Objects that record how they're called and let you verify interactions. "Was this method called? With what arguments?"

Stubs: Objects that return predetermined responses. "When get_user() is called, return this specific user object."

Spies: Real objects wrapped to record calls while maintaining actual behavior. "Call the real function but track that it happened."

Fakes: Simplified working implementations for testing. "Use an in-memory database instead of PostgreSQL."

Python's unittest.mock provides Mock objects that can act as any of these types.

unittest.mock Basics

Python's standard library includes unittest.mock, a powerful mocking framework. No external packages needed for basic mocking.

Creating a Mock:

Mocks accept any method calls and attribute access. They're incredibly flexible, which is both powerful and dangerous.

Mock vs MagicMock: MagicMock is a subclass of Mock that includes magic methods (__str__, __len__, etc.) by default. Use MagicMock when you need to mock operators or built-in functions.

For most cases, use Mock. Use MagicMock only when you need magic method support.

Setting Return Values and Side Effects

return_value specifies what a mock returns when called:

from unittest.mock import Mock

mock_database = Mock()
mock_database.get_user.return_value = {"id": 1, "name": "Alice"}

result = mock_database.get_user(1)
assert result == {"id": 1, "name": "Alice"}

side_effect lets you specify exceptions, sequences, or custom behavior:

# Raise an exception
mock_api = Mock()
mock_api.fetch.side_effect = ConnectionError("API unreachable")

# This raises ConnectionError
# mock_api.fetch()

# Return different values for successive calls
mock_counter = Mock()
mock_counter.next.side_effect = [1, 2, 3]

assert mock_counter.next() == 1
assert mock_counter.next() == 2
assert mock_counter.next() == 3

# Use a function for dynamic behavior
def custom_behavior(x):
    return x * 2

mock_calc = Mock()
mock_calc.double.side_effect = custom_behavior

assert mock_calc.double(5) == 10

Patching: Replacing Objects During Tests

Patching temporarily replaces an object with a mock for the duration of a test. This is crucial for testing code that imports and uses external dependencies.

The @patch Decorator:

from unittest.mock import patch

# Code being tested (in production_code.py)
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/{city}")
    return response.json()

# Test
@patch('production_code.requests.get')
def test_get_weather(mock_get):
    # Configure the mock
    mock_get.return_value.json.return_value = {"temp": 72, "conditions": "sunny"}

    result = get_weather("San Francisco")

    assert result["temp"] == 72
    mock_get.assert_called_once_with("https://api.weather.com/San Francisco")

Important: Patch where the object is used, not where it's defined. If production_code.py imports requests, patch production_code.requests, not requests itself.

Multiple Patches:

@patch('module.database')
@patch('module.cache')
def test_with_multiple_mocks(mock_cache, mock_database):
    # Note: mocks are passed in reverse order of decorators
    mock_database.get.return_value = {"user": "Alice"}
    mock_cache.get.return_value = None

    # Your test code here

patch.object for patching specific methods:

class EmailService:
    def send(self, to, subject, body):
        # Real implementation sends email
        pass

@patch.object(EmailService, 'send')
def test_email_sending(mock_send):
    service = EmailService()
    service.send("user@example.com", "Test", "Body")

    mock_send.assert_called_once()

Verifying Mock Calls

Mocks record every interaction. You can verify exactly how your code used them.

Common Assertions:

mock = Mock()
mock.some_method(1, 2, key="value")

# Was it called?
assert mock.some_method.called
assert mock.some_method.call_count == 1

# Was it called with specific arguments?
mock.some_method.assert_called_with(1, 2, key="value")
mock.some_method.assert_called_once_with(1, 2, key="value")

# What were all the calls?
print(mock.some_method.call_args)  # Last call arguments
print(mock.some_method.call_args_list)  # All calls

# Verify it was NOT called
never_called_mock = Mock()
never_called_mock.assert_not_called()

assert_called_once_with is stricter than assert_called_with. The former ensures exactly one call; the latter only checks the last call.

Mocking External Dependencies

Real-world example: testing a function that sends notifications.

# notification_service.py
import smtplib

def send_notification(email, message):
    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login('user@example.com', 'password')
    server.sendmail('user@example.com', email, message)
    server.quit()
    return True

# test_notification_service.py
from unittest.mock import patch, Mock

@patch('notification_service.smtplib.SMTP')
def test_send_notification(mock_smtp):
    # Configure mock server
    mock_server = Mock()
    mock_smtp.return_value = mock_server

    # Call the function
    result = send_notification('recipient@example.com', 'Hello!')

    # Verify SMTP interactions
    mock_smtp.assert_called_once_with('smtp.gmail.com', 587)
    mock_server.starttls.assert_called_once()
    mock_server.login.assert_called_once()
    mock_server.sendmail.assert_called_once()
    mock_server.quit.assert_called_once()

    assert result == True

This test verifies the notification logic without sending real emails. It's fast, doesn't require network access, and won't spam anyone.

Best Practices for Mocking

Mock at the Boundaries: Mock external systems (APIs, databases, file systems), not internal logic. If you're mocking your own code, your design might be too coupled. External dependencies are natural seams for mocking. Your business logic should be tested directly without mocks—only the external interactions need mocking.

Don't Over-Mock: Excessive mocking makes tests brittle. If changing implementation breaks tests that still verify correct behavior, you're mocking too much. Good tests verify what code does, not how it does it. Mocking internals couples tests to implementation details, making refactoring painful.

Use Real Objects When Possible: For fast, simple dependencies, use real implementations. Only mock when necessary for speed or isolation. If a class is pure Python with no I/O, use the real class. Testing with real objects gives more confidence and avoids mock/reality mismatches.

Make Mocks Realistic: Mocks should behave like the real thing. If your real API returns paginated results, your mock should too. If the real API can timeout, your mock should simulate that. Unrealistic mocks give false confidence—tests pass but production fails because reality behaves differently.

Verify Interactions Carefully: Don't assert every call. Verify the important interactions—that the right data was saved, the right API was called with correct parameters, etc. Over-specification makes tests fragile. Focus on observable behavior and critical interactions.

Name Mocks Descriptively: Use names like mock_user_api or mock_database instead of generic mock1, mock2. Clear names make test failures easier to debug and tests easier to understand.

Reset Mocks Between Tests: If sharing mocks across tests, reset them. Better yet, create fresh mocks for each test. Shared state between tests causes mysterious failures.

Keep Mock Configuration Close to Usage: Configure mocks near where they're used in the test. This makes tests self-documenting. If configuration is far from usage, tests become hard to understand.

Common Mocking Mistakes

Mocking Everything: Mocking internal functions defeats the purpose of integration. Mock boundaries, not internals. If you mock five internal functions to test one function, you're not really testing anything—you're testing that mocks return what you configured them to return. Only mock external boundaries like databases, APIs, and file systems.

Unrealistic Mocks: If your mock returns data the real system never would, tests pass but production fails. For example, if your API never returns null for a user ID lookup but your mock does, tests work but production breaks. Study the real system's behavior and make mocks match it precisely.

Testing the Mock: Verifying the mock's behavior, not your code's behavior. Tests should verify your logic, not that mocks work. If your test just checks mock.method.called, you're testing the mock library, not your code. Focus assertions on business logic outcomes.

Wrong Patch Target: Patching where objects are defined instead of where they're used. Patch the import location, not the source. If payment_processor.py imports stripe, patch payment_processor.stripe, not stripe directly. This is one of the most common mocking errors.

Ignoring Mock Realism: Real APIs have rate limits, pagination, errors. Mocks that ignore these give false confidence. If production handles pagination but your mock returns all results at once, you never test pagination logic. Model real-world complexity in your mocks.

Not Testing Error Paths: Developers often mock happy paths and forget errors. What happens when the API returns 500? When the database connection fails? Use side_effect to test error handling thoroughly.

Leaking Mock State: Mocks retain state across tests if not reset. One test's mock.call_count affects the next test. Always create fresh mocks or reset them between tests to avoid mysterious failures.

When to Mock vs. When to Integrate

Mock When: Testing would be slow (network calls, database queries), unreliable (external APIs), expensive (paid services), or destructive (sending emails, charging cards). Mocking isolates your code from these dependencies.

Don't Mock When: Dependencies are fast and deterministic. Pure Python classes, simple utilities, and mathematical operations don't need mocking. Testing with real objects gives better coverage and catches integration issues.

The 80/20 Rule: About 80% of your tests should be fast unit tests that mock external dependencies. About 20% should be integration tests using real dependencies. This balance gives good coverage without sacrificing speed.

Practical Tips for Effective Mocking

Start Small: When learning mocking, start with simple examples. Mock a single function call before attempting complex scenarios. Master return_value before diving into side_effect and complex behaviors.

Read Documentation: Real systems have documentation. Read it before mocking. Understanding actual API behavior helps create realistic mocks. If the API paginates results, your mock should too.

Use Type Hints: Type hints make mocks easier to create and verify. If your function expects UserService, creating a mock with the right methods is straightforward.

Consider Dependency Injection: If you find patching difficult, your code might be too tightly coupled. Dependency injection—passing dependencies as parameters—makes testing easier and mocking simpler.

Test Integration Points: Even with extensive mocking, test integration points with real dependencies occasionally. Integration tests catch mismatches between your mocks and reality. Schedule regular integration testing to verify mocks stay accurate.

Course Recommendations

Advanced Python Testing

  • Comprehensive mocking strategies
  • Test doubles and dependency injection
  • Integration vs unit testing approaches
  • Enroll at paiml.com

Test-Driven Development Mastery

  • TDD with external dependencies
  • Mocking best practices
  • Real-world TDD projects
  • Enroll at paiml.com

Clean Architecture with Python

  • Designing testable systems
  • Dependency inversion for easier testing
  • SOLID principles in practice
  • Enroll at paiml.com

Quiz

📝 Test Your Knowledge: Mocking and Patching

Take this quiz to reinforce what you've learned in this chapter.