unittest.mock, from stand-ins to spies

Python · Testing

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

  1. Basics — what a mock is and how to tell it what to say
  2. patch — put a mock in place, then put the original back
  3. 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:

NameWhat it doesIn unittest.mock
StubReturns canned answers. Records nothing you look atMock(return_value=…)
FakeA small working implementation (an in-memory store)Your own class; side_effect= a function
MockCanned answers and a record of every call, checked afterwardsMock() + assert_called_…
SpyRecords how it was used, for checking afterwards — with wraps=, while the real code still runsMock(wraps=real)

One object covers all four. The smallest example:

first_mock.py
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")
$ python first_mock.py
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.

children.py
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))
$ python children.py
AB12 CDE
call(7)
[call.plates.read(7)]
True
The same convenience is the main danger

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:

answers.py
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"))
$ python answers.py
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.

A list runs out

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.

magic.py
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)
$ python magic.py
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.

specs.py
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")
$ python specs.py
plain Mock: no error
spec: Mock object has no attribute 'sned'
autospec: missing a required argument: 'subject'
autospec: the right call passes
FormUnknown attributeWrong arguments
Mock()AcceptedAccepted
Mock(spec=Cls)AttributeErrorAccepted
create_autospec(Cls) / patch(…, autospec=True)AttributeErrorTypeError
Autospec is what keeps a mock honest

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:

greet.py
from datetime import datetime

def greeting():
    return "good morning" if datetime.now().hour < 12 else "good afternoon"
three_ways.py
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)
$ python three_ways.py
with       : good morning
decorator  : good morning
start/stop : good morning
restored   : True
start() without stop() leaks

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.

where.py
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)
$ python where.py
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 saysIt callsPatch
from mailer import sendsend(…)"myapp.service.send" — the name in the module that uses it
import mailermailer.send(…)"mailer.send" — the attribute is looked up at call time
import timetime.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.

patch_object.py
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
$ python patch_object.py
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.

env.py
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)
$ python env.py
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.

files.py
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)
$ python files.py
version=3
opened: call('settings.conf')
A real temporary file is often simpler

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.

retry.py
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)
test_retry.py
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
$ pytest -q test_retry.py
..                                                                       [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:

PartWhat is in it
3 · Spieswraps=: 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 · CheckingEvery assert_called_… method, call, call_args_list, mock_calls, ANY, AsyncMock
5 · ReferenceEight traps (including the assert that checks nothing), mock with pytest's monkeypatch, a one-screen cheat sheet
Read the rest

unittest.mock, from stand-ins to spies — MareArts Open Lab. Open Lab articles are free to read with a MareArts account.

Every runnable snippet verified against CPython 3.11.5 and pytest 8.4.2.