Filter concepts by levelShowing all levels.

Python · Testing

Mocking

Concepts
5

unittest.mock's Mock, MagicMock, patch, and AsyncMock; monkeypatching; the vocabulary of test doubles (stubs, spies, fakes); and the roadmap's own "most importantly" rule — knowing what should and should not be mocked.

This section

The mocking toolkit

unittest.mock's core objects, temporarily swapping a name with patch, and pytest's built-in monkeypatch fixture.

Mock and MagicMock

coreintermediate

Mock() creates a stand-in object: any attribute or method call on it just returns another Mock, and it records every call so a test can check it later. MagicMock is the same, but also supports dunder methods like __len__ and __iter__.

Think of it as

A Mock is a notepad shaped like an object. Call any method on it, and instead of doing real work, it writes down "you called me with these arguments" and hands back a pre-set (or default) answer — a test can read the notepad afterward.

python
from unittest.mock import Mock, MagicMock

m = Mock(return_value=42)
m(1, 2)                       # returns 42
m.assert_called_once_with(1, 2)

mm = MagicMock()
mm.__len__.return_value = 5
len(mm)                        # 5 -- plain Mock has no __len__

What we're doing: Create a Mock, call it, and inspect what it recorded — then show MagicMock supporting a dunder method plain Mock does not.

mock_basics.pypython
from unittest.mock import Mock, MagicMock

mock_send = Mock(return_value=True)
result = mock_send("user@example.com", "hello")
print("result:", result)
mock_send.assert_called_once_with("user@example.com", "hello")
print("call_count:", mock_send.call_count)

m = MagicMock()
m.__len__.return_value = 5
print("len(m):", len(m))
3
Mock(return_value=True) means every call to mock_send returns True, regardless of arguments.
4
This call is recorded — the mock does no real work, but remembers it was called with these two arguments.
10
__len__ is a dunder method — plain Mock() would raise TypeError here; MagicMock implements it by default.
Output
result: True
call_count: 1
len(m): 5

Why this works: Mock records every call for later inspection (assert_called_once_with, call_count) instead of doing real work. MagicMock adds the dunder protocol methods Python's built-in functions (len(), iter(), with) rely on, which plain Mock does not implement.

Calling len() on a plain Mock, expecting it to work like MagicMock

Wrong

python
from unittest.mock import Mock

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

Better

python
from unittest.mock import MagicMock

m = MagicMock()
m.__len__.return_value = 3
print(len(m))   # 3

What you see: TypeError: object of type 'Mock' has no len()

Why: Mock only auto-creates ordinary attributes and methods, not the dunder methods Python's built-ins call directly (len() calls __len__, not a regular method). MagicMock pre-configures the common dunders so they work out of the box.

Mock vs. MagicMock — one extra capability

Mock()

records calls, auto-creates attributes

assert_called_once_with(...)

reads back what was recorded

MagicMock()

same, plus __len__, __iter__, __enter__

  • Mock() — records calls, auto-creates attributes
  • assert_called_once_with(...) — reads back what was recorded
  • MagicMock() — same, plus __len__, __iter__, __enter__

Mock — assertions worth knowing

Mock — assertions worth knowing
CallChecks
mock.assert_called()was called at least once
mock.assert_called_once()was called exactly once
mock.assert_called_once_with(*a, **kw)called exactly once, with these exact arguments
mock.assert_not_called()was never called
mock.call_countthe number of times it was called, as an int

Together

python
mock_send = Mock(return_value=True)
result = mock_send("user@example.com", "hello")
mock_send.assert_called_once_with("user@example.com", "hello")
print(result, mock_send.call_count)

Remember: Mock() records calls and returns whatever you configure; MagicMock() does the same plus supports dunder methods like __len__ that plain Mock does not.

See also: patch and asyncmock · what to mock

patch and AsyncMock

coreintermediate

patch("module.name") temporarily replaces a name in a module's namespace with a Mock, for the duration of a with block or test, then restores the original automatically. AsyncMock is the version whose return value is itself awaitable.

Think of it as

patch is borrowing a library book and returning it automatically when you close the with block — the real object is swapped out for a mock only for the duration you specify, and swapped back after, even if the test raises.

python
from unittest.mock import patch, AsyncMock

with patch("mymodule.send_email") as mocked:
    mocked.return_value = "sent"
    ...   # send_email is a Mock here, restored automatically after

fetcher.fetch = AsyncMock(return_value={"status": "ok"})
result = await fetcher.fetch(url)
fetcher.fetch.assert_awaited_once_with(url)

What we're doing: Patch a module-level function for the duration of a test, and separately confirm AsyncMock supports await and awaited-call assertions.

test_patch_asyncmock.pypython
from unittest.mock import Mock, AsyncMock, patch
import pytest

def send_welcome_email(user_id, mailer):
    return mailer.send(user_id, "Welcome!")

def test_patch_replaces_target():
    with patch("test_patch_asyncmock.send_welcome_email") as mocked:
        mocked.return_value = "patched"
        result = send_welcome_email(1, Mock())
        assert result == "patched"

class Fetcher:
    async def fetch(self, url):
        raise ConnectionError("real network call")

async def test_asyncmock():
    fetcher = Fetcher()
    fetcher.fetch = AsyncMock(return_value={"status": "ok"})
    result = await fetcher.fetch("https://api.example.com")
    assert result == {"status": "ok"}
    fetcher.fetch.assert_awaited_once_with("https://api.example.com")
8
patch's target is the DOTTED PATH to where the name lives in this test module's own namespace, not the function object itself.
18
Assigning an AsyncMock to fetch means calling it returns a coroutine — real code with await fetcher.fetch(url) does not need to change at all.
Output
2 passed

Why this works: patch swaps send_welcome_email for a Mock only inside the with block, restoring the real function afterward automatically. AsyncMock makes fetch() return something awaitable, so real await-based code exercises the mock exactly like it would exercise the real coroutine.

patch swaps a name out, then back — even on failure

with patch("mymodule.send_email")

name replaced with a Mock

test body runs against the Mock

real function restored

automatic — even if the test raised

  • with patch("mymodule.send_email")
    • leads to name replaced with a Mock
  • name replaced with a Mock
    • leads to test body runs against the Mock
  • test body runs against the Mock
    • leads to real function restored
  • real function restored — automatic — even if the test raised

Patching where a function is defined instead of where it is used

Wrong

python
# myapp.py: from mymodule import send_email
with patch("mymodule.send_email") as mocked:
    myapp.notify_user("user@example.com")
    # ConnectionError: real network call -- the patch had no effect

Better

python
# patch the name where myapp actually looks it up
with patch("myapp.send_email") as mocked:
    myapp.notify_user("user@example.com")   # mocked correctly

What you see: The mock is never actually used — the real function still runs, exactly as if patch had not been called at all.

Why: from mymodule import send_email creates a SEPARATE reference in myapp's own namespace. Patching mymodule.send_email leaves that copy untouched — patch must target the name where the calling code actually looks it up.

patch — the forms worth knowing

patch — the forms worth knowing
FormUse
with patch("mod.name") as m:scoped to the with block only
@patch("mod.name")scoped to the whole decorated test function
patch.object(obj, "method")patches one method on an already-imported object
AsyncMock(return_value=X)an awaitable mock — await mock() returns X

Together

python
with patch("myapp.email.send_welcome_email") as mocked:
    mocked.return_value = "patched"
    result = myapp.email.send_welcome_email(1)
    assert result == "patched"

Remember: patch("module.name") swaps a name for a Mock and restores it; AsyncMock is the awaitable version.

See also: mock and magicmock · awaitables tasks and futures

Monkeypatching

standardintermediate

monkeypatch is a built-in pytest fixture for temporarily changing something — an attribute, an environment variable, a dict entry — for the duration of one test, undoing the change automatically afterward, even if the test fails.

Think of it as

monkeypatch is patch with automatic cleanup built in and no context manager needed — just request it as a fixture parameter, make changes through it, and pytest reverses every one when the test ends.

python
def test_uses_env_var(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key-123")
    assert get_api_key() == "test-key-123"
    # API_KEY is restored to whatever it was before, automatically

What we're doing: Temporarily set an environment variable with monkeypatch and confirm code reading it sees the patched value.

test_monkeypatch.pypython
import os

def get_api_key():
    return os.environ.get("API_KEY")


def test_monkeypatch_env_var(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key-123")
    assert get_api_key() == "test-key-123"
7
monkeypatch.setenv sets the real os.environ entry for the duration of this test only — no manual cleanup needed.
Output
1 passed

Why this works: monkeypatch.setenv actually sets os.environ["API_KEY"] for real, so any code reading it — including code that does not know it is under test — sees the patched value. pytest reverts it automatically once the test function returns.

Remember: monkeypatch is a built-in fixture for temporary changes that automatically undo after the test.

See also: patch and asyncmock · fixtures and scopes

Advertisement

Using mocks well

The vocabulary for different kinds of test double, and the discipline — called out by the roadmap as "most importantly" — of mocking the right thing.

Test doubles: stubs, spies, and fakes

standardintermediate

A stub returns canned answers and nothing more. A spy is a real (or wrapped) object that also records how it was called. A fake is a working, simplified implementation — like an in-memory dict standing in for a real database.

Think of it as

These are all "test doubles" — stand-ins for a real dependency — but they differ in how much they actually do. A stub is a cardboard cutout with a scripted line. A spy is a real actor being filmed. A fake is an understudy who can actually perform the part, just more simply.

python
class FakeRepository:               # a working, simplified implementation
    def __init__(self):
        self._items = {}
    def save(self, item):
        self._items[item["id"]] = item
    def get(self, item_id):
        return self._items.get(item_id)

What we're doing: Write a fake repository — an in-memory stand-in for a real database — and use it in a test exactly like the real thing would be used.

fake_repository.pypython
class FakeUserRepository:
    def __init__(self):
        self._users = {}

    def save(self, user):
        self._users[user["id"]] = user

    def get(self, user_id):
        return self._users.get(user_id)


def register_user(repository, user_id, name):
    user = {"id": user_id, "name": name}
    repository.save(user)
    return user


def test_register_user_saves_to_repository():
    repo = FakeUserRepository()   # no real database needed
    register_user(repo, 1, "Ada")
    saved = repo.get(1)
    print("saved user:", saved)
    assert saved == {"id": 1, "name": "Ada"}
2
FakeUserRepository genuinely stores and retrieves data — it just uses a dict instead of a real database connection.
20
register_user is tested against a fake that behaves like the real repository's interface, without needing an actual database.
Output
saved user: {'id': 1, 'name': 'Ada'}
1 passed

Why this works: A fake genuinely implements save/get, so this test exercises real save-then-retrieve logic — closer to an integration test than a pure unit test with a stub, but still fast because there is no real database.

The four kinds, by what they actually do

The four kinds, by what they actually do
DoubleDoes real work?Records calls?
StubNo — returns a fixed answerNo
SpyOften yes — wraps something realYes
FakeYes — a simplified real implementationNo (usually)
Mock (unittest.mock)No, unless configured toYes

Together

python
# Stub -- fixed answer, nothing recorded
def stub_get_price(item_id):
    return 9.99

# Fake -- a simplified but genuinely working implementation
class FakeUserRepository:
    def __init__(self):
        self._users = {}
    def save(self, user):
        self._users[user["id"]] = user
    def get(self, user_id):
        return self._users.get(user_id)

# Spy -- wraps a real call and records it
class SpyLogger:
    def __init__(self, real_logger):
        self.real_logger = real_logger
        self.calls = []
    def log(self, message):
        self.calls.append(message)
        self.real_logger.log(message)

Remember: Stubs return canned answers; spies wrap and record real work; fakes are simplified-but-working implementations.

See also: mock and magicmock · what to mock

Knowing what should (and should not) be mocked

coreintermediate

Mock external dependencies you do not control — network calls, databases, payment gateways, the clock. Do not mock the code you are actually testing — that proves nothing except that the mock does what you told it to.

Think of it as

Mock at the boundary of your system, not inside it. A boundary is where your code hands off to something external — an HTTP call, a database write. Mocking something INSIDE your own logic just tests that your mock agrees with itself.

python
# GOOD -- mock the external boundary
def test_places_order(mock_payment_gateway):
    place_order(cart, mock_payment_gateway)
    mock_payment_gateway.charge.assert_called_once()

# BAD -- mocks the thing actually under test
def test_calculate_total():
    calc = Mock()
    calc.calculate_total.return_value = 100
    assert calc.calculate_total() == 100   # tests nothing real

What we're doing: Show a test that mocks the wrong thing (the function under test) passing even after that function is broken, versus a correctly-scoped test that would actually catch the bug.

over_mocking.pypython
def calculate_total(price, quantity):
    return price + quantity   # BUG: should be price * quantity


def test_over_mocked_hides_the_bug():
    from unittest.mock import Mock
    fake_calculate_total = Mock(return_value=30)
    # this "tests" a mock, not the real calculate_total function
    result = fake_calculate_total(10, 3)
    assert result == 30   # PASSES -- but says nothing about the real bug


def test_correctly_scoped_catches_the_bug():
    result = calculate_total(10, 3)
    # correctly calls the REAL function -- exposes the actual bug
    assert result == 30  # FAILS: 10 + 3 = 13, not 30
7
This mock is standing in for calculate_total itself, the exact thing this test claims to check — the real function is never called.
15
This test calls the real, buggy function — and correctly fails, because it is not testing a mock's own configured answer.
Output
test_over_mocked_hides_the_bug PASSED
test_correctly_scoped_catches_the_bug FAILED
E       assert 13 == 30

Why this works: The over-mocked test passes regardless of what calculate_total actually does, because it never calls the real function — it only checks that a Mock returns what it was told to return. The correctly-scoped test calls the real implementation and catches the actual bug (+ instead of *).

Mocking a dependency so thoroughly the test no longer exercises real logic

Wrong

python
def test_apply_discount():
    order = Mock()
    order.apply_discount.return_value = 90
    assert order.apply_discount(10) == 90
    # tests the Mock's own configuration, not apply_discount's real logic

Better

python
def test_apply_discount():
    order = Order(price=100)   # the REAL class
    order.apply_discount(10)   # the REAL method
    assert order.price == 90   # checks REAL behavior

What you see: The test suite stays green through a refactor that silently breaks real behavior — mocks were configured to return the "right" answer regardless of what the real code does.

Why: A mock only proves it returns what it was configured to return — it never runs the real logic underneath. Mocking the thing under test (rather than its external dependencies) removes the only part of the test that could actually catch a bug.

Mock the boundary, never the thing under test

Mock external dependencies

  • +Network calls, databases, payment gateways, the clock
  • +Keeps tests fast, deterministic, no real side effects
  • +The real logic under test still runs

Mocking the code under test

  • The function the test exists to check is replaced too
  • Passes even after that function is broken — proves nothing
  • A test that still passes after deleting the real code went too far
  • Mock external dependencies
    • Network calls, databases, payment gateways, the clock
    • Keeps tests fast, deterministic, no real side effects
    • The real logic under test still runs
  • Mocking the code under test
    • The function the test exists to check is replaced too
    • Passes even after that function is broken — proves nothing
    • A test that still passes after deleting the real code went too far

Remember: Mock external boundaries (network, database, payment, clock) — never mock the code the test exists to actually check, or the test proves nothing.

See also: mock and magicmock · test doubles vocabulary

Advertisement