unittest.mock, from stand-ins to spies
A mock is a stand-in you put where a real function or object used to be, for as long as a test
runs. It answers the way you tell it to, it remembers every call it got, and when the test ends the
original comes back. This guide covers both halves — controlling and recording — and then the
part most tutorials skip: wraps=, a spy that lets the real code run.
Some things a test cannot afford to do for real: charge a card, send an e-mail, wait thirty seconds
for a retry, ask the clock what time it is. Other things a test cannot see: whether an
inner function received the argument it should have, when the final answer comes out the same
either way. unittest.mock is the standard library's answer to both, and it ships with
every Python 3.
Part 1 is the Mock object itself. Part 2 is patch, which puts a mock in place and
takes it out again — including the one rule about where to patch that everybody learns
the hard way. Both are here. Parts 3 to 5 — spying with wraps=, checking what happened,
the traps and a one-screen cheat sheet — are on MareArts Open Lab.
Every runnable snippet here was executed with CPython 3.11.5 and pytest 8.4.2 before publishing, and every output block is copied from those runs. Anything version-sensitive is flagged inline.
Contents
- Basics — what a mock is and how to tell it what to say
- patch — put a mock in place, then put the original back
- Spies, checking, traps — Parts 3 to 5, on MareArts Open Lab
Part 1 · Basics
What a mock is
"Mock" gets used for every kind of test stand-in. The older vocabulary, from Gerard Meszaros'
xUnit Test Patterns, is more precise, and it maps neatly onto what
unittest.mock can do:
| Name | What it does | In unittest.mock |
|---|---|---|
| Stub | Returns canned answers. Records nothing you look at | Mock(return_value=…) |
| Fake | A small working implementation (an in-memory store) | Your own class; side_effect= a function |
| Mock | Canned answers and a record of every call, checked afterwards | Mock() + assert_called_… |
| Spy | Records how it was used, for checking afterwards — with wraps=, while the real code still runs | Mock(wraps=real) |
One object covers all four. The smallest example:
from unittest.mock import Mock
pay = Mock(return_value="ok") # a stand-in that always says "ok"
print(pay(30, currency="EUR")) # call it like the real thing
print(pay.called, pay.call_count) # it remembers that it was called ...
print(pay.call_args) # ... and with what
pay.assert_called_once_with(30, currency="EUR")
print("assert passed")
ok
True 1
call(30, currency='EUR')
assert passed
The Mock object
A Mock invents attributes on demand. Ask for anpr.plates.read and you get a child
mock, which is itself callable and has its own children. That is what lets one mock stand in for
a whole API client without you declaring its shape.
from unittest.mock import Mock
anpr = Mock() # stands in for a MareArts ANPR client
anpr.plates.read.return_value = {"id": 7, "plate": "AB12 CDE"}
result = anpr.plates.read(7)
print(result["plate"])
print(anpr.plates.read.call_args) # the child remembers its own calls
print(anpr.mock_calls) # the parent remembers everything below it
print(callable(anpr.not_defined_yet))
AB12 CDE
call(7)
[call.plates.read(7)]
True
anpr.not_defined_yet did not raise. A plain Mock accepts any attribute name,
including typos and methods your real class does not have. spec and autospec
fix that, and you should reach for them by default.
return_value and side_effect
return_value is one fixed answer. side_effect is everything else, and it takes
three kinds of value:
from unittest.mock import Mock
# 1. a fixed answer
fetch = Mock(return_value=200)
print(fetch(), fetch())
# 2. a list: one answer per call, in order
fetch = Mock(side_effect=[500, 500, 200])
print(fetch(), fetch(), fetch())
# 3. an exception: raised when called
fetch = Mock(side_effect=TimeoutError("slow upstream"))
try:
fetch()
except TimeoutError as e:
print("raised:", e)
# 4. a function: called with the same arguments, its result is returned
fetch = Mock(side_effect=lambda url: 404 if "missing" in url else 200)
print(fetch("/ok"), fetch("/missing"))
200 200
500 500 200
raised: slow upstream
200 404
A list may mix values and exceptions — [TimeoutError, TimeoutError, {"ok": True}] is the
classic "fails twice, then works" for retry code. When side_effect is set,
it wins over return_value.
The fourth call to Mock(side_effect=[500, 500, 200]) raises StopIteration. If the
code under test calls more often than you expected, that is the error you will see — and inside a
generator it can be swallowed silently. Make the list exactly as long as the calls you mean.
MagicMock
Mock does not support Python's protocol methods — len(), [],
with, iteration. MagicMock does, with sensible defaults, and each one can be
configured like any other child.
from unittest.mock import Mock, MagicMock
m = MagicMock()
m.__len__.return_value = 3
m.__getitem__.side_effect = lambda key: key.upper()
print(len(m), m["key"])
with m as ctx: # the context-manager protocol is ready too
print(ctx is m.__enter__.return_value)
try:
len(Mock())
except TypeError as e:
print("plain Mock:", e)
3 KEY
True
plain Mock: object of type 'Mock' has no len()
patch creates a MagicMock unless told otherwise, so in practice that is the one you
meet most often.
spec and autospec
A spec tells the mock what the real thing looks like. With spec=, attributes the real class
does not have raise AttributeError. With autospec, the signatures are copied too, so
a call with the wrong arguments fails the way it would in production.
from unittest.mock import Mock, create_autospec
class Mailer:
def send(self, to, subject, body=""):
...
loose = Mock()
loose.sned("ada") # typo: a plain Mock accepts it
print("plain Mock: no error")
strict = Mock(spec=Mailer)
try:
strict.sned("ada")
except AttributeError as e:
print("spec:", e)
auto = create_autospec(Mailer, instance=True)
try:
auto.send("ada") # 'subject' is missing
except TypeError as e:
print("autospec:", e)
auto.send("ada", "hi")
auto.send.assert_called_once_with("ada", "hi")
print("autospec: the right call passes")
plain Mock: no error
spec: Mock object has no attribute 'sned'
autospec: missing a required argument: 'subject'
autospec: the right call passes
| Form | Unknown attribute | Wrong arguments |
|---|---|---|
Mock() | Accepted | Accepted |
Mock(spec=Cls) | AttributeError | Accepted |
create_autospec(Cls) / patch(…, autospec=True) | AttributeError | TypeError |
When someone renames send or adds a required argument, a loose mock keeps every test
green while production breaks. An autospecced mock fails the same tests on the same day. The cost
is a little speed on very large classes, which is almost never worth saving.
Part 2 · patch
Three ways to patch
A mock on its own is only useful if the code under test receives it as an argument. Most code does
not — it imports what it needs. patch replaces a name inside a module for a limited time
and puts the original back afterwards, even when the test fails.
The module under test asks the clock what time it is, which a test cannot control:
from datetime import datetime
def greeting():
return "good morning" if datetime.now().hour < 12 else "good afternoon"
from datetime import datetime
from unittest.mock import patch
import greet
MORNING = datetime(2026, 1, 1, 9, 0)
# 1. context manager: the patch lives for the with block
with patch("greet.datetime") as fake:
fake.now.return_value = MORNING
print("with :", greet.greeting())
# 2. decorator: the patch lives for one call; the mock arrives as an argument
@patch("greet.datetime")
def check(fake):
fake.now.return_value = MORNING
return greet.greeting()
print("decorator :", check())
# 3. start/stop: when the patch must outlive one block (setUp / tearDown)
p = patch("greet.datetime")
fake = p.start()
fake.now.return_value = MORNING
print("start/stop :", greet.greeting())
p.stop()
print("restored :", greet.datetime is datetime)
with : good morning
decorator : good morning
start/stop : good morning
restored : True
The context manager and the decorator undo themselves. start() does not — if the
stop() line is never reached, the patch stays for every test that runs after. In a
unittest.TestCase, register it: self.addCleanup(p.stop) right after
p.start().
Where to patch
This is the rule that costs everyone an afternoon once. greet.py did
from datetime import datetime, so the module greet holds its own name
datetime, bound when it was imported. Replacing the name somewhere else changes nothing
that greet can see.
from datetime import datetime
from unittest.mock import patch
import greet
with patch("datetime.datetime") as fake: # where it is defined
print("datetime.datetime -> greet sees the mock:", greet.datetime is fake)
with patch("greet.datetime") as fake: # where it is looked up
print("greet.datetime -> greet sees the mock:", greet.datetime is fake)
try: # and the method itself?
patch("datetime.datetime.now", return_value=datetime(2026, 1, 1, 9)).start()
except TypeError as e:
print("datetime.datetime.now ->", e)
datetime.datetime -> greet sees the mock: False
greet.datetime -> greet sees the mock: True
datetime.datetime.now -> cannot set 'now' attribute of immutable type 'datetime.datetime'
| The code under test says | It calls | Patch |
|---|---|---|
from mailer import send | send(…) | "myapp.service.send" — the name in the module that uses it |
import mailer | mailer.send(…) | "mailer.send" — the attribute is looked up at call time |
import time | time.sleep(…) | "time.sleep" |
open(…) (a builtin) | open(…) | "builtins.open", or "myapp.service.open" with create=True |
The last line of the output is the other classic: types written in C, like datetime, do not
let you replace their methods at all. Replace the name that holds the type instead, as
patch("greet.datetime") did.
patch.object
patch("greet.datetime") takes a dotted string that is resolved only when the patch starts.
patch.object takes the object itself and an attribute name: the module is an ordinary import
at the top of the test file, so your editor can follow it and a rename tool updates it. Either form
refuses an attribute that does not exist.
from datetime import datetime
from unittest.mock import patch
import greet
with patch.object(greet, "datetime") as fake: # same effect as patch("greet.datetime")
fake.now.return_value = datetime(2026, 1, 1, 15, 0)
print(greet.greeting())
try:
with patch.object(greet, "datetme"): # misspelt: refused, not silently added
pass
except AttributeError as e:
print("refused:", str(e).rsplit("> ", 1)[-1]) # the message starts with the module's path
good afternoon
refused: does not have the attribute 'datetme'
Both forms take the same keyword arguments: return_value, side_effect,
autospec, wraps, new. Part 3, on Open Lab, uses patch.object throughout.
patch.dict
For dictionaries — most often os.environ. The changes are undone on exit, including keys you
added and keys you removed.
import os
from unittest.mock import patch
def site():
return os.environ.get("MAREARTS_SITE", "https://www.marearts.com")
with patch.dict(os.environ, {"MAREARTS_SITE": "http://localhost:8000"}):
print(site())
print(site())
with patch.dict(os.environ, {}, clear=True): # an empty environment
print("inside :", len(os.environ), "variables")
print("outside:", len(os.environ) > 0)
http://localhost:8000
https://www.marearts.com
inside : 0 variables
outside: True
mock_open
mock_open builds a MagicMock that behaves like open() and the file object
it returns, including with, read, readline and iteration.
from unittest.mock import mock_open, patch
def first_line(path):
with open(path) as f:
return f.readline().strip()
m = mock_open(read_data="version=3\nname=demo\n")
with patch("builtins.open", m):
print(first_line("settings.conf"))
m.assert_called_once_with("settings.conf")
print("opened:", m.call_args)
version=3
opened: call('settings.conf')
With pytest, the tmp_path fixture gives you a real empty directory per test. Writing a
real file there exercises the real open, encoding included, and needs no patching. Keep
mock_open for the cases where the path cannot be redirected.
A real test
Everything so far, together, in a pytest file. The code retries on timeouts and sleeps between attempts; the test must not actually sleep, and must prove the sleeps happened.
import time
def fetch_with_retry(get, url, attempts=3, delay=1.0):
for i in range(attempts):
try:
return get(url)
except TimeoutError:
if i == attempts - 1:
raise
time.sleep(delay)
from unittest.mock import Mock, call, patch
import pytest
from retry import fetch_with_retry
def test_retries_then_succeeds():
get = Mock(side_effect=[TimeoutError, TimeoutError, {"ok": True}])
with patch("time.sleep") as sleep: # retry.py calls time.sleep(...)
assert fetch_with_retry(get, "/x") == {"ok": True}
assert get.call_count == 3
assert sleep.call_args_list == [call(1.0), call(1.0)]
def test_gives_up_after_three():
get = Mock(side_effect=TimeoutError)
with patch("time.sleep"), pytest.raises(TimeoutError):
fetch_with_retry(get, "/x")
assert get.call_count == 3
.. [100%]
2 passed in 0.01s
get is passed in, so it needs no patching at all — a plain Mock argument. Only
time.sleep, which the code reaches by itself, needs patch. Designing code to take
its collaborators as arguments is the cheapest way to need fewer patches.
Continue on MareArts Open Lab
That is the half that replaces things. The other half — watching the real code run, and checking what it did — is on MareArts Open Lab, with the same runnable examples and real outputs:
| Part | What is in it |
|---|---|
| 3 · Spies | wraps=: the real function runs and every call is recorded. When only a spy can tell — an argument that never changes the result. Hand-rolled swap vs patch.object. Spying on a method without losing self |
| 4 · Checking | Every assert_called_… method, call, call_args_list, mock_calls, ANY, AsyncMock |
| 5 · Reference | Eight traps (including the assert that checks nothing), mock with pytest's monkeypatch, a one-screen cheat sheet |
unittest.mock, from stand-ins to spies — MareArts Open Lab. Open Lab articles are free to read with a MareArts account.