mocking-patterns.md 6.6 KB

Mocking Patterns

Comprehensive guide to mocking in pytest.

unittest.mock Basics

Mock Object

from unittest.mock import Mock

def test_mock_basics():
    mock = Mock()

    # Access any attribute (auto-created)
    mock.some_attribute
    mock.method()
    mock.nested.deeply.value

    # Configure return values
    mock.get_data.return_value = {"key": "value"}
    assert mock.get_data() == {"key": "value"}

    # Check calls
    mock.get_data.assert_called_once()
    mock.get_data.assert_called_with()  # No args

MagicMock

from unittest.mock import MagicMock

def test_magic_mock():
    mock = MagicMock()

    # Supports magic methods
    mock.__len__.return_value = 5
    assert len(mock) == 5

    # Iteration
    mock.__iter__.return_value = iter([1, 2, 3])
    assert list(mock) == [1, 2, 3]

    # Context manager
    mock.__enter__.return_value = "entered"
    with mock as m:
        assert m == "entered"

patch Decorator

from unittest.mock import patch

# Patch where used, not where defined
@patch("mymodule.requests.get")
def test_api_call(mock_get):
    mock_get.return_value.json.return_value = {"status": "ok"}

    result = mymodule.fetch_data()

    assert result["status"] == "ok"
    mock_get.assert_called_once_with("https://api.example.com/data")

# Multiple patches (applied bottom-up)
@patch("mymodule.save_to_db")
@patch("mymodule.fetch_from_api")
def test_multiple_patches(mock_fetch, mock_save):  # Note: reverse order
    mock_fetch.return_value = {"data": []}
    process_and_save()
    mock_save.assert_called_once()

patch Context Manager

from unittest.mock import patch

def test_with_context_manager():
    with patch("mymodule.external_service") as mock_service:
        mock_service.call.return_value = "mocked"
        result = mymodule.do_work()
        assert result == "mocked"

    # After context, original is restored

patch.object

from unittest.mock import patch

class MyClass:
    def method(self):
        return "real"

def test_patch_object():
    obj = MyClass()

    with patch.object(obj, "method", return_value="mocked"):
        assert obj.method() == "mocked"

    assert obj.method() == "real"  # Restored

patch.dict

from unittest.mock import patch
import os

def test_patch_dict():
    with patch.dict(os.environ, {"API_KEY": "test-key"}):
        assert os.environ["API_KEY"] == "test-key"

    # Clear and add
    with patch.dict(os.environ, {"NEW_VAR": "value"}, clear=True):
        assert "PATH" not in os.environ
        assert os.environ["NEW_VAR"] == "value"

side_effect

from unittest.mock import Mock

def test_side_effect_function():
    mock = Mock()
    mock.side_effect = lambda x: x * 2
    assert mock(5) == 10

def test_side_effect_exception():
    mock = Mock()
    mock.side_effect = ValueError("Invalid input")

    with pytest.raises(ValueError):
        mock()

def test_side_effect_list():
    mock = Mock()
    mock.side_effect = [1, 2, ValueError("Done")]

    assert mock() == 1
    assert mock() == 2
    with pytest.raises(ValueError):
        mock()

spec and autospec

from unittest.mock import Mock, create_autospec

class RealAPI:
    def get_user(self, user_id: int) -> dict:
        pass

    def create_user(self, name: str) -> dict:
        pass

def test_with_spec():
    # Only allows methods that exist on RealAPI
    mock = Mock(spec=RealAPI)
    mock.get_user(1)  # OK
    # mock.invalid_method()  # AttributeError

def test_with_autospec():
    # Also validates signatures
    mock = create_autospec(RealAPI)
    mock.get_user(1)  # OK
    # mock.get_user("string")  # Still OK at runtime, but IDE warns
    # mock.get_user(1, 2, 3)  # TypeError: too many args

pytest-mock Plugin

# pip install pytest-mock

def test_with_mocker(mocker):
    # mocker is a fixture that wraps unittest.mock
    mock = mocker.patch("mymodule.external_call")
    mock.return_value = "mocked"

    result = mymodule.process()

    assert result == "mocked"
    mock.assert_called_once()

def test_spy(mocker):
    # Spy: call real method but track calls
    spy = mocker.spy(mymodule, "helper_function")

    mymodule.main_function()

    spy.assert_called()
    # Original function was actually called

def test_stub(mocker):
    # Stub: quick attribute replacement
    mocker.patch.object(MyClass, "expensive_method", return_value="cheap")

Async Mocking

from unittest.mock import AsyncMock

async def test_async_mock():
    mock = AsyncMock()
    mock.return_value = {"async": "result"}

    result = await mock()

    assert result == {"async": "result"}
    mock.assert_awaited_once()

@patch("mymodule.async_fetch", new_callable=AsyncMock)
async def test_patch_async(mock_fetch):
    mock_fetch.return_value = {"data": []}

    result = await mymodule.get_data()

    assert result == {"data": []}

PropertyMock

from unittest.mock import PropertyMock, patch

class MyClass:
    @property
    def value(self):
        return "real"

def test_property_mock():
    with patch.object(
        MyClass, "value", new_callable=PropertyMock
    ) as mock_prop:
        mock_prop.return_value = "mocked"
        obj = MyClass()
        assert obj.value == "mocked"

Common Patterns

Mock HTTP Response

def test_mock_response(mocker):
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"id": 1}
    mock_response.raise_for_status = Mock()

    mocker.patch("requests.get", return_value=mock_response)

    result = fetch_user(1)
    assert result["id"] == 1

Mock File Operations

from unittest.mock import mock_open, patch

def test_file_read():
    m = mock_open(read_data="file content")
    with patch("builtins.open", m):
        result = read_config("config.txt")
        assert "content" in result

def test_file_write():
    m = mock_open()
    with patch("builtins.open", m):
        write_data("output.txt", "data")
        m().write.assert_called_with("data")

Mock datetime

from unittest.mock import patch
from datetime import datetime

def test_mock_datetime(mocker):
    mock_dt = mocker.patch("mymodule.datetime")
    mock_dt.now.return_value = datetime(2024, 1, 15, 12, 0, 0)

    result = mymodule.get_timestamp()
    assert "2024-01-15" in result

Best Practices

  1. Patch where used - Not where defined
  2. Use autospec - Catch API mismatches
  3. Reset mocks - In fixtures or with mock.reset_mock()
  4. Don't over-mock - Test behavior, not implementation
  5. Prefer dependency injection - Over patching
  6. Use pytest-mock - Cleaner syntax than unittest.mock