py4u guide

Mocking with Python’s unittest.mock: A Guide

Unit testing is a cornerstone of reliable software development, ensuring that individual components of your code work as expected. However, many functions and classes in your codebase may interact with external systems—such as APIs, databases, or file systems—or depend on other parts of your codebase. Testing these components directly can be slow, flaky (due to external dependencies), or even impossible (if the external system isn’t available). This is where **mocking** comes to the rescue. Mocking is a technique where you replace real objects with controlled substitutes (called "mocks") that simulate the behavior of the real objects. By using mocks, you isolate the code under test, ensuring your tests focus *only* on the logic of the component being tested, not its dependencies. Python’s `unittest.mock` library (built into Python 3.3+) is a powerful tool for creating and managing mocks. It provides a flexible framework to replace external dependencies, verify interactions (e.g., "was this function called with the right arguments?"), and simulate return values or exceptions. In this guide, we’ll dive deep into `unittest.mock`, exploring its core components, practical use cases, advanced techniques, and best practices. By the end, you’ll be equipped to write robust, isolated unit tests with confidence.

Table of Contents

  1. What is unittest.mock?
  2. Key Components of unittest.mock
  3. Basic Usage: Creating and Configuring Mocks
  4. Using patch() to Replace Objects
  5. Common Mocking Scenarios
  6. Advanced Techniques
  7. Best Practices for Effective Mocking
  8. Conclusion
  9. References

What is unittest.mock?

unittest.mock is a Python standard library module designed to simplify the creation of mock objects for unit testing. It allows you to:

  • Replace external dependencies (e.g., APIs, databases) with controlled substitutes.
  • Verify that certain functions/methods were called with the expected arguments.
  • Simulate return values, exceptions, or complex behaviors (e.g., returning different values on successive calls).

Unlike third-party mocking libraries (e.g., pytest-mock), unittest.mock is built into Python, so no additional installation is required. It integrates seamlessly with Python’s built-in unittest framework and works with pytest as well.

Key Components of unittest.mock

Mock and MagicMock

The Mock class is the heart of unittest.mock. It creates flexible mock objects that can be called like functions, have attributes, and track interactions (e.g., how many times they were called).

MagicMock is a subclass of Mock that includes default implementations for Python’s “magic methods” (e.g., __len__, __getitem__, __iter__). Use MagicMock when you need to mock objects that rely on magic methods (e.g., lists, dictionaries, or custom classes with magic methods).

from unittest.mock import Mock, MagicMock

# Basic Mock
mock = Mock()
mock()  # Call the mock
mock.assert_called_once()

# MagicMock (supports magic methods out of the box)
magic_mock = MagicMock()
magic_mock.__len__.return_value = 5
assert len(magic_mock) == 5  # Works because __len__ is mocked

patch()

The patch() function is used to temporarily replace an object in your code with a mock during testing. It’s the most commonly used tool in unittest.mock because it lets you isolate the code under test by swapping out external dependencies.

patch() can be used as a decorator, context manager, or manually (via start()/stop()), and it automatically cleans up after itself to avoid polluting other tests.

PropertyMock

PropertyMock is used to mock properties (attributes defined with the @property decorator) or other descriptors. It allows you to set return values for attribute access or verify that the property was accessed.

AsyncMock

Introduced in Python 3.8, AsyncMock is designed to mock asynchronous functions (coroutines). It mimics the behavior of async def functions, allowing you to await mock calls and verify interactions with async code.

Basic Usage: Creating and Configuring Mocks

Setting Return Values

Mocks can return predefined values when called. Use return_value to set a fixed return value:

from unittest.mock import Mock

def test_mock_return_value():
    # Create a mock and set its return value
    mock = Mock()
    mock.return_value = "Hello, Mock!"

    # Call the mock
    result = mock()

    # Verify the result
    assert result == "Hello, Mock!"
    # Verify the mock was called once
    mock.assert_called_once()

Using side_effect

side_effect is more flexible than return_value. It can:

  • Return different values on successive calls (using an iterable).
  • Raise exceptions.
  • Call a custom function to compute the return value.

Example 1: Return Different Values

mock = Mock()
mock.side_effect = [1, 2, 3]  # First call returns 1, second 2, etc.

assert mock() == 1
assert mock() == 2
assert mock() == 3

Example 2: Raise an Exception

mock = Mock()
mock.side_effect = ValueError("Oops!")

try:
    mock()
except ValueError as e:
    assert str(e) == "Oops!"  # Exception is raised
mock.assert_called_once()

Example 3: Custom Function

def add(a, b):
    return a + b

mock = Mock()
mock.side_effect = add  # Delegate to the add function

assert mock(2, 3) == 5  # mock(2,3) calls add(2,3)
mock.assert_called_once_with(2, 3)

Verifying Calls

Mocks track every call made to them, allowing you to verify:

  • If the mock was called (assert_called()).
  • How many times it was called (assert_called_once(), call_count).
  • What arguments it was called with (assert_called_with(), call_args).
mock = Mock()

# Call the mock with arguments
mock("foo", bar=42)

# Verify calls
mock.assert_called_once()
mock.assert_called_with("foo", bar=42)
assert mock.call_count == 1
assert mock.call_args == (("foo",), {"bar": 42})  # Tuple of (args, kwargs)

Using patch() to Replace Objects

The “Where to Patch” Rule

A common pitfall is patching an object in the wrong place. The rule is:
Patch the object where it is imported, not where it is defined.

Example: Suppose you have:

my_project/
├── my_module.py
└── test_my_module.py

my_module.py imports requests and uses requests.get:

# my_module.py
import requests

def fetch_data(url):
    response = requests.get(url)  # Uses requests.get
    return response.json()

To test fetch_data, you need to patch requests.get as imported in my_module, not the global requests module. Thus, you patch my_module.requests.get.

Patch as a Decorator, Context Manager, or Manual

1. Patch as a Decorator

# test_my_module.py
from unittest.mock import patch
from my_module import fetch_data

@patch("my_module.requests.get")  # Patch my_module's requests.get
def test_fetch_data(mock_get):
    # Configure the mock response
    mock_response = Mock()
    mock_response.json.return_value = {"key": "value"}
    mock_get.return_value = mock_response  # requests.get returns mock_response

    # Call the function under test
    result = fetch_data("https://api.example.com")

    # Verify interactions
    assert result == {"key": "value"}
    mock_get.assert_called_once_with("https://api.example.com")  # Check URL
    mock_response.json.assert_called_once()  # Ensure .json() was called

2. Patch as a Context Manager

Use a context manager if you only need the mock for a specific block of code:

def test_fetch_data_context_manager():
    with patch("my_module.requests.get") as mock_get:
        mock_response = Mock()
        mock_response.json.return_value = {"key": "value"}
        mock_get.return_value = mock_response

        result = fetch_data("https://api.example.com")
        assert result == {"key": "value"}

3. Manual Patch (start/stop)

For fine-grained control, start and stop the patch manually:

def test_fetch_data_manual():
    patcher = patch("my_module.requests.get")
    mock_get = patcher.start()  # Start patching

    try:
        mock_response = Mock()
        mock_response.json.return_value = {"key": "value"}
        mock_get.return_value = mock_response
        result = fetch_data("https://api.example.com")
        assert result == {"key": "value"}
    finally:
        patcher.stop()  # Always stop to clean up!

Patching Objects and Multiple Dependencies

Patch an Object’s Method with patch.object

Use patch.object() to patch a specific method of an object:

class MyClass:
    def method(self):
        return "Real value"

def test_patch_object():
    obj = MyClass()
    with patch.object(obj, "method", return_value="Mocked value"):
        assert obj.method() == "Mocked value"  # Method is patched
    assert obj.method() == "Real value"  # Patch is removed after context

Patch Multiple Dependencies with patch.multiple

Use patch.multiple() to patch several objects at once:

from unittest.mock import patch, Mock

def test_multiple_patches():
    with patch.multiple(
        "my_module",
        func1=Mock(return_value=1),
        func2=Mock(return_value=2)
    ) as mocks:
        # mocks is a dict of patched objects: {"func1": mock1, "func2": mock2}
        assert my_module.func1() == 1
        assert my_module.func2() == 2
        mocks["func1"].assert_called_once()

Common Mocking Scenarios

Mocking External APIs

Let’s expand the earlier fetch_data example to test error handling (e.g., HTTP 404):

# test_my_module.py
from unittest.mock import patch, Mock
from my_module import fetch_data

def test_fetch_data_404_error():
    with patch("my_module.requests.get") as mock_get:
        # Mock a 404 response
        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
        mock_get.return_value = mock_response

        # Test that fetch_data raises an error
        try:
            fetch_data("https://api.example.com/invalid")
        except requests.exceptions.HTTPError as e:
            assert "404 Not Found" in str(e)
        mock_get.assert_called_once_with("https://api.example.com/invalid")

Mocking Database Interactions

Suppose you have a function that queries a database with sqlite3:

# my_db_module.py
import sqlite3

def get_user(user_id):
    conn = sqlite3.connect("mydb.db")
    cursor = conn.cursor()
    cursor.execute("SELECT name FROM users WHERE id = ?", (user_id,))
    user = cursor.fetchone()
    conn.close()
    return user[0] if user else None

To test get_user without a real database, mock sqlite3.connect, cursor.execute, and cursor.fetchone:

# test_my_db_module.py
from unittest.mock import patch, Mock
from my_db_module import get_user

@patch("my_db_module.sqlite3.connect")
def test_get_user(mock_connect):
    # Mock the connection and cursor
    mock_conn = Mock()
    mock_cursor = Mock()
    mock_connect.return_value = mock_conn
    mock_conn.cursor.return_value = mock_cursor

    # Mock fetchone to return a user
    mock_cursor.fetchone.return_value = ("Alice",)

    # Call the function under test
    result = get_user(1)

    # Verify interactions
    assert result == "Alice"
    mock_connect.assert_called_once_with("mydb.db")
    mock_conn.cursor.assert_called_once()
    mock_cursor.execute.assert_called_once_with(
        "SELECT name FROM users WHERE id = ?", (1,)
    )
    mock_cursor.fetchone.assert_called_once()
    mock_conn.close.assert_called_once()

Mocking File System Operations

To test code that reads/writes files without touching the real file system, mock open() using patch("builtins.open"):

# file_utils.py
def read_file(filename):
    with open(filename, "r") as f:
        return f.read()

Test it with:

# test_file_utils.py
from unittest.mock import patch, mock_open
from file_utils import read_file

def test_read_file():
    # Mock open() and set its read data
    mock_file = mock_open(read_data="Hello, World!")
    with patch("builtins.open", mock_file):
        content = read_file("test.txt")
        assert content == "Hello, World!"
        mock_file.assert_called_once_with("test.txt", "r")
        mock_file().read.assert_called_once()  # Check that read() was called

mock_open is a convenience function that mocks the open() context manager.

Advanced Techniques

Mocking Properties with PropertyMock

Use PropertyMock to mock attributes that are properties:

class MyClass:
    @property
    def value(self):
        return 42  # Real property

def test_property_mock():
    with patch("__main__.MyClass.value", new_callable=PropertyMock) as mock_prop:
        mock_prop.return_value = 99
        obj = MyClass()
        assert obj.value == 99  # Property returns mock value
        mock_prop.assert_called_once()  # Verify the property was accessed

Async Mocking with AsyncMock

For async code, use AsyncMock to mock coroutines:

# async_module.py
import aiohttp

async def async_fetch_data(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

Test it with AsyncMock:

# test_async_module.py
from unittest.mock import patch, AsyncMock
import pytest
from async_module import async_fetch_data

@pytest.mark.asyncio
async def test_async_fetch_data():
    with patch("async_module.aiohttp.ClientSession") as mock_session_cls:
        # Mock ClientSession and its get method
        mock_session = AsyncMock()
        mock_session_cls.return_value.__aenter__.return_value = mock_session

        # Mock the response
        mock_response = AsyncMock()
        mock_response.json.return_value = {"data": "mocked"}
        mock_session.get.return_value.__aenter__.return_value = mock_response

        # Await the async function
        result = await async_fetch_data("https://api.example.com")

        assert result == {"data": "mocked"}
        mock_session.get.assert_awaited_once_with("https://api.example.com")
        mock_response.json.assert_awaited_once()

Strict Mocks with spec and spec_set

By default, mocks allow accessing any attribute (e.g., mock.invalid_attr returns another mock). To enforce that mocks only have attributes/methods present on a real object (to catch typos), use spec or spec_set:

  • spec: Mocks can’t have attributes not in the spec, but existing attributes can be modified.
  • spec_set: Mocks can’t have new attributes or modify existing ones (stricter).
class RealClass:
    def method(self):
        pass

# Create a mock with spec=RealClass
mock = Mock(spec=RealClass)
mock.method()  # OK (method exists in RealClass)
mock.invalid_attr  # Raises AttributeError (invalid_attr not in RealClass)

Best Practices for Effective Mocking

  1. Keep Mocks Simple: Only mock what you need. Over-mocking makes tests brittle and hard to read.

  2. Test Behavior, Not Implementation: Focus on what the code does, not how it does it. Avoid asserting internal calls unless critical (e.g., “did the API get called?” vs. “did helper_function get called?”).

  3. Avoid Over-Mocking: Don’t mock core language features (e.g., list, dict) or simple helper functions. Mock only external dependencies (APIs, databases) or slow operations.

  4. Use spec for Safety: Use spec to ensure mocks match the interface of real objects, catching typos like mock.gett() instead of mock.get().

  5. Clean Up Patches: patch() automatically cleans up, but if using manual start()/stop(), always stop patches in a finally block to avoid test pollution.

  6. Document Mock Intentions: In complex tests, add comments explaining why a mock is used (e.g., “Mocking API to avoid network calls”).

Conclusion

unittest.mock is a powerful library that simplifies writing isolated, reliable unit tests by replacing external dependencies with controlled mocks. By mastering its core components—Mock, MagicMock, patch(), and advanced tools like AsyncMock—you can test code that interacts with APIs, databases, or file systems without leaving your test suite.

Remember to follow best practices: keep mocks focused, test behavior over implementation, and use spec to enforce interface correctness. With these skills, you’ll write tests that are fast, deterministic, and easy to maintain.

References