backend / testing / pytest / 05_mock_vs_magicmock_patch.md

Mock, MagicMock, patch

4 min read source

Mock, MagicMock, patch

unittest.mock is the standard mocking library. Three things to keep straight: what to instantiate, how to install it, and where to install it.

Mock vs MagicMock

from unittest.mock import Mock, MagicMock

m = Mock()
m.foo()         # OK — auto-creates `.foo`, returns another Mock
m()             # OK — m is callable
len(m)          # TypeError: object of type 'Mock' has no len()
m["key"]        # TypeError

mm = MagicMock()
len(mm)         # 0 — supports __len__
mm["key"]       # MagicMock — supports __getitem__
mm + 1          # MagicMock — supports __add__

MagicMock pre-configures magic methods (dunders): __len__, __iter__, __getitem__, __contains__, __enter__, __exit__, etc. Use it for objects that need to behave like containers, context managers, or numerics.

Mock is leaner — use when you don’t need dunders.

return_value vs side_effect

m = Mock()
m.return_value = 42
m()  # 42
m()  # 42

return_value is what the mock returns when called.

side_effect is more flexible:

# Iterable: each call pops the next value
m.side_effect = [1, 2, 3]
m(); m(); m()        # 1, 2, 3
m()                   # raises StopIteration

# Exception class or instance: raised
m.side_effect = ValueError("boom")
m()                   # raises ValueError

# Callable: invoked with the call args
m.side_effect = lambda x: x * 2
m(5)                  # 10

When both are set, side_effect wins, unless side_effect returns DEFAULT, in which case return_value is used.

patch — replace at a location

from unittest.mock import patch

@patch("myapp.api.requests.get")
def test_call(mock_get):
    mock_get.return_value.json.return_value = {"ok": True}
    result = myapp.api.fetch()
    assert result == {"ok": True}

patch is a context manager / decorator that swaps an attribute on a module for the duration. Decorator argument order is reverse of decorator stacking:

@patch("myapp.b")
@patch("myapp.a")
def test(mock_a, mock_b):  # innermost decorator → first arg
    ...

patch where it’s used, not where it’s defined

This is the #1 mock gotcha.

# myapp/api.py
import requests
def fetch():
    return requests.get("https://...").json()
# Wrong: patches the requests module, but myapp.api already imported it
@patch("requests.get")
def test_fetch(mock_get): ...

# Right: patches the name as it lives in myapp.api
@patch("myapp.api.requests.get")
def test_fetch(mock_get): ...

from x import y binds y in the importing module’s namespace. Patching x.y changes the original; the importing module still references the old object.

patch.object

When you have the object in a variable, not a string path:

import myapp.api as api
with patch.object(api, "requests") as mock_requests:
    mock_requests.get.return_value.json.return_value = {"ok": True}
    api.fetch()

Equivalent to patch("myapp.api.requests") but the path is a Python expression rather than a string. Type checkers like it better.

patch.dict

For mutable mappings (notably os.environ):

@patch.dict("os.environ", {"API_KEY": "test-key"})
def test_api():
    ...

# Or as context manager:
with patch.dict(os.environ, {"API_KEY": "test"}, clear=True):
    ...

clear=True empties the dict before applying the patch. Restored on exit.

autospec — type-safety mocks

@patch("myapp.api.requests.get", autospec=True)
def test(mock_get):
    mock_get(123, badarg="x")   # TypeError: requests.get got unexpected kw 'badarg'

autospec=True inspects the real function’s signature. Calls with wrong arguments fail at test time, just like in production. Highly recommended.

For classes, autospec also restricts which attributes/methods exist:

@patch("myapp.client.HTTPClient", autospec=True)
def test(MockClient):
    instance = MockClient.return_value
    instance.connect()        # OK
    instance.nonexistent()    # AttributeError

spec vs autospec

spec=SomeClass constrains the mock’s attributes to match the class. autospec=True also auto-spec’s signatures. spec_set= is spec with attribute setting also blocked.

In practice: prefer autospec=True. It catches more bugs.

AsyncMock

For coroutines, use AsyncMock. Pre-configured to return coroutines.

from unittest.mock import AsyncMock

@patch("myapp.api.fetch_data", new_callable=AsyncMock)
async def test_call(mock_fetch):
    mock_fetch.return_value = {"ok": True}
    result = await myapp.api.fetch_data()
    assert result == {"ok": True}

In Python 3.8+ MagicMock auto-detects async methods on a spec and uses AsyncMock for them.

See 06_pytest_asyncio.md.

Asserting calls

mock.assert_called()                             # called at least once
mock.assert_called_once()                        # exactly once
mock.assert_called_with(1, 2, key="x")            # last call args
mock.assert_called_once_with(1, 2)               # exactly once + args
mock.assert_any_call(...)                        # any historical call

mock.call_count                                   # int
mock.call_args                                    # last call's call(args, kwargs)
mock.call_args_list                              # all calls

Common typo bug: assert_called_with requires the underscore — assert mock.called_with(...) silently auto-creates a sub-mock and always passes. Use autospec=True (or from unittest.mock import call) to catch this.

Common patterns

Mock returning a chained call:

mock_get.return_value.json.return_value = {"ok": True}
# now: mock_get(...).json() == {"ok": True}

Recording call sequences:

mock.call_args_list == [
    call(1, 2),
    call(3, 4),
]

Reset between assertions:

mock.reset_mock()  # clears call history; keeps return_value/side_effect

Interview angle

  • Q: “Difference between Mock and MagicMock?” — MagicMock pre-configures dunders.
  • Q: “What’s return_value vs side_effect?” — fixed value vs callable/exception/iterable.
  • Follow-up: “Where do you patch — at definition or use site?” — use site. patch("myapp.api.requests.get") not patch("requests.get").
  • Follow-up: “What does autospec=True give you?” — signature checking, attribute restriction, catches “called with wrong args” bugs.

See 01_mocks_external_apis.md for end-to-end mock examples, 09_test_doubles_taxonomy.md for the broader taxonomy.