Safeguard
DevSecOps

Mocking in Python: unittest.mock, MagicMock, and Return Values

A practitioner's guide to Python unit test mocking: when to use Mock vs MagicMock, setting return values and side effects, patching in the right place, and the assertions that make a mock worth writing.

Karan Patel
Platform Engineer
7 min read

A Python unit test mock is a stand-in object from the unittest.mock library that records how it was called and returns whatever you tell it to, letting you test one unit of code in isolation from the network, the filesystem, the clock, or any dependency that is slow, flaky, or has side effects. The standard library ships everything you need — Mock, MagicMock, and patch — so mocking in Python rarely means adding a dependency. What it does require is knowing which tool fits, where to patch, and how to set return values without building a test that passes while the code is broken.

Mock vs MagicMock: the practical difference

Both auto-create attributes and methods on access, so you never have to declare them. The difference is that MagicMock also implements Python's "magic" dunder methods (__len__, __getitem__, __enter__, __iter__, and friends), while plain Mock does not.

from unittest.mock import Mock, MagicMock

m = Mock()
len(m)          # TypeError: object of type 'Mock' has no len()

mm = MagicMock()
len(mm)         # 0  — MagicMock supports __len__
with mm as ctx: # works — MagicMock supports the context-manager protocol
    ...

So what is MagicMock in one sentence: it is Mock plus working dunder methods, which is why patch uses MagicMock as its default. Practical rule — reach for MagicMock when the thing you are replacing gets used as a context manager, an iterable, or with len()/indexing; use Mock when you want an error if the code touches a dunder you did not expect, which occasionally catches real bugs.

Setting a Python mock return value

The single most common thing you will do. A mock's return_value is what it hands back when called:

from unittest.mock import Mock

client = Mock()
client.fetch.return_value = {"status": "ok", "count": 3}

result = client.fetch("/health")
assert result["count"] == 3

Every call returns the same value. When you need different results across calls, or you need the mock to raise, use side_effect:

# Different value per call — an iterable is consumed one item at a time
client.fetch.side_effect = [{"count": 1}, {"count": 2}, TimeoutError()]

# A callable computes the return dynamically from the arguments
def fake_lookup(key):
    return {"a": 1, "b": 2}[key]
cache.get.side_effect = fake_lookup

# An exception class or instance makes the call raise
db.connect.side_effect = ConnectionError("refused")

side_effect takes precedence, with one nuance: if the callable returns unittest.mock.DEFAULT, the mock falls back to return_value. And when side_effect is a list, each call pops the next item — a clean way to model a retry that fails twice then succeeds.

patch: replace the real thing, in the right place

patch swaps a name with a mock for the duration of a test, then restores it. The rule that trips up everyone once: patch where the name is looked up, not where it is defined.

# app/service.py
from app.clients import PaymentClient

def charge(amount):
    return PaymentClient().submit(amount)

To mock PaymentClient in this test, you patch it at app.service, because that is the namespace charge resolves it in:

from unittest.mock import patch

@patch("app.service.PaymentClient")     # correct — where it is used
def test_charge(MockClient):
    MockClient.return_value.submit.return_value = "txn_123"
    assert charge(100) == "txn_123"
    MockClient.return_value.submit.assert_called_once_with(100)

Patching app.clients.PaymentClient instead would leave charge pointing at the real class, and the test would silently hit the real payment path. Note the double return_value: MockClient is the class, MockClient.return_value is the instance it produces, and .submit.return_value is what the method returns.

The decorator, context-manager, and patch.object forms are interchangeable — pick by readability:

def test_with_cm():
    with patch("app.service.time") as mock_time:
        mock_time.time.return_value = 1_700_000_000
        ...

@patch.object(PaymentClient, "submit", return_value="txn_9")
def test_object(_):
    ...

Assert the interaction, not just the result

A mock that is never asserted against tests almost nothing. The value of a Python unittest mock example is in verifying the contract between your code and its dependency:

client.submit.assert_called_once()
client.submit.assert_called_once_with(amount=100, currency="usd")
client.submit.assert_not_called()

# Order across multiple calls
from unittest.mock import call
client.log.assert_has_calls([call("start"), call("done")])

# Inspect raw call records for complex assertions
args, kwargs = client.submit.call_args
assert kwargs["currency"] == "usd"

Watch for the assertion-typo trap: any attribute you access on a mock exists, so mock.assert_called_once() (real) and mock.assert_called_once accessed without calling it, or a misspelled mock.asert_called_once(), both evaluate truthy and pass. Guard against it with autospec, below.

spec and autospec: stop mocks from lying

A bare mock accepts any attribute and any call signature, so it will happily pass tests for methods that no longer exist. create_autospec and patch(..., autospec=True) constrain the mock to the real object's actual API:

from unittest.mock import patch

@patch("app.service.PaymentClient", autospec=True)
def test_signature_enforced(MockClient):
    instance = MockClient.return_value
    instance.submit("only-one-arg")   # real submit takes (amount);
    # a wrong signature or a typo'd method name now raises AttributeError/TypeError

Autospec is the single highest-value habit in mocking Python code. Without it, a refactor that renames submit to submit_charge leaves every mock-based test green while production breaks. This matters beyond tidiness: over-mocked test suites give a false sense of security precisely because mocks confirm the code you wrote, not the code that runs — a green suite over hollow mocks has repeatedly let regressions reach production. Tests are load-bearing infrastructure; they deserve the same scrutiny as the code they cover. Structured material on building test discipline into a secure development workflow lives in the Safeguard Academy.

When not to mock

Mocking has a cost: every mock encodes an assumption about how a collaborator behaves, and if that assumption drifts from reality, your test lies to you. Guidelines that keep suites honest:

  • Do not mock what you do not own without a thin adapter. Mocking a third-party client's internals couples your tests to their implementation details; wrap it and mock your wrapper.
  • Prefer fakes for stateful things. An in-memory dict is a better test double for a cache than a MagicMock with hand-scripted return_values.
  • Do not mock the thing under test. If you find yourself patching internals of the function you are testing, the function is doing too much — that is a design signal, not a testing problem.

FAQ

What is the difference between Mock and MagicMock?

MagicMock is Mock with all the magic dunder methods (__len__, __iter__, __enter__, __getitem__, etc.) pre-configured. Use MagicMock when your code treats the double as a context manager, iterable, or container; use plain Mock when you want unexpected dunder access to fail loudly.

How do I make a mock return different values on each call?

Set side_effect to a list: mock.side_effect = [1, 2, 3]. Each call pops the next value, and a list entry that is an exception is raised instead of returned. For logic based on arguments, set side_effect to a function.

Where should I patch — the module that defines a name or uses it?

Where it is used. Patch the name in the namespace of the module under test (app.service.PaymentClient), not where the class was originally defined (app.clients.PaymentClient), because the code under test looks the name up in its own module.

Why do my mock assertions always pass?

Because accessing any attribute on a mock creates it, so a misspelled or un-called assertion like mock.asert_called_once() is truthy and never fails. Use autospec=True (or create_autospec) so the mock enforces the real object's attributes and call signatures.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.