Filter concepts by levelShowing all levels.

Django · Section 67

Mocking

Level
advanced
Read
28 min
Concepts
3

`Mock` accepts any attribute and any call and records what happened; `MagicMock` adds the dunder methods, which is why `patch` returns one by default; `AsyncMock` returns coroutines for anything awaited. The rule that decides whether any of it works is that you patch where a name is **used**, not where it is defined — `from billing import charge_card` copies the reference into the importing module, so patching the defining module rebinds a name your code no longer reads. The rule that decides whether it is *safe* is `autospec=True`: without it a double fabricates whatever you ask for, so `charge.called_once_with(...)` — an assertion missing its `assert_` prefix — is just another passing mock call. The vocabulary of doubles is a scale of how much a test depends on implementation: a stub returns canned answers, a fake is a working simplified implementation (Django ships two you already use, `locmem` email and `LocMemCache`), a spy wraps the real thing and records, and a mock carries expectations. Fakes and stubs assert on outcomes and survive refactors; a strict mock asserts the interaction and is right only when the interaction is the requirement. Which leads to the section's own warning, stated outright: a test that passes only because every dependency is mocked provides very little confidence. Mock at the boundary — anything leaving the process, anything nondeterministic — and never the ORM, your models, your services, or the database, because those are what the test exists to exercise. Then assert on observable outcomes rather than on which internal helpers were called, so a behaviour-preserving refactor stays green.

What is true here

  1. Patch where the name is looked up, not where the function is defined.
  2. autospec=True turns a fabricated attribute or a wrong signature into an error instead of a pass.
  3. Stub, fake, spy and mock form a scale of how much the test depends on implementation.
  4. Mock what leaves the process and what is nondeterministic; never the ORM or the database.
  5. Assert on outcomes; reserve interaction assertions for cases where the call itself is the requirement.

What you will be able to do

  • Choose the right double class and patch target so the replacement actually intercepts the call
  • Use `autospec` and `AsyncMock` correctly, and recognise the assertions that silently do nothing
  • Write a fake rather than a mock where the dependency has a small stable interface
  • Draw the mocking boundary so a test still fails when the code stops working
Where the mocking boundary belongs, from your code outward

Your models and ORM

never mocked — query construction is where the bugs are, and a mock agrees with whatever you wrote

Your service functions

never mocked — replacing them means the test verifies your mock configuration

The test database

real, with rollback isolation. Fast enough, and far more truthful than any double.

Django-shipped fakes

locmem email and LocMemCache — real implementations, so the test proves the behaviour works

Nondeterminism: now(), random, uuid4

freeze it — a test must not be able to fail on a Tuesday

THE BOUNDARY — anything leaving the process

payment provider, SMTP, SMS, object storage. Mock here and nowhere closer in.

The outside world

never reached in a test — which is the entire point of drawing the line above

  1. Your models and ORM — never mocked — query construction is where the bugs are, and a mock agrees with whatever you wrote
  2. Your service functions — never mocked — replacing them means the test verifies your mock configuration
  3. The test database — real, with rollback isolation. Fast enough, and far more truthful than any double.
  4. Django-shipped fakes — locmem email and LocMemCache — real implementations, so the test proves the behaviour works
  5. Nondeterminism: now(), random, uuid4 — freeze it — a test must not be able to fail on a Tuesday
  6. THE BOUNDARY — anything leaving the process — payment provider, SMTP, SMS, object storage. Mock here and nowhere closer in.
  7. The outside world — never reached in a test — which is the entire point of drawing the line above

The toolkit

Mock, MagicMock, AsyncMock, patch and monkeypatch — and the two rules that make them work.

Mock, MagicMock, patch, AsyncMock, and monkeypatching

coreintermediate

`Mock` is an object that accepts any attribute access and any call, records what happened, and returns another `Mock`. `MagicMock` is the same with the dunder methods configured, so it supports `len()`, iteration, `in`, and context managers — which is why it is what `patch` gives you by default. `AsyncMock` returns a coroutine when called, so it stands in for anything you `await`. `patch` temporarily replaces an attribute for the duration of a test and puts the original back afterwards, and pytest's `monkeypatch` fixture does the same for attributes, dictionary entries and environment variables. The single rule that decides whether any of this works: **patch where the name is used, not where it is defined**.

Think of it as

A `Mock` says yes to everything, which is exactly what makes it useful and exactly what makes it dangerous. `mock.anything.at.all()` succeeds, so an assertion that is not quite an assertion — `charge.called_once_with(...)`, missing the `assert_` prefix — is just another mock call that returns a truthy object and passes. Modern `unittest.mock` catches the obvious version by raising for attributes starting with `assert`, which is precisely why the surviving mistake is the one that drops the prefix. `autospec=True` fixes the whole class by building the replacement from the real object's signature, so a wrong argument count or a nonexistent method raises the way it would in production; treat it as the default rather than an option. The patch-location rule follows from how imports work: `from services import charge` binds `charge` into the *importing* module's namespace, so patching `services.charge` afterwards rebinds a name your code is no longer reading. Patching `orders.views.charge` — the name at the place it is looked up — is what actually intercepts the call. And the choice between `patch` and `monkeypatch` is mostly ergonomic: `patch` is the standard-library tool with `autospec`, spec assertions and call recording, while `monkeypatch` is pytest's fixture and is nicer for `setenv`, `setitem` and `delattr` — both undo themselves at the end of the test, which is the property that matters.

python
with patch("orders.views.charge_card", autospec=True) as charge:
    charge.side_effect = PaymentDeclined("insufficient funds")
    ...

What we're doing: Replace the one call that leaves the process, with a double that cannot silently accept a wrong call.

orders/tests/test_checkout.pypython
# orders/services.py does:  from billing.stripe import charge_card

@pytest.mark.django_db
def test_successful_checkout(order):
    with patch("orders.services.charge_card", autospec=True) as charge:
        charge.return_value = Charge(id="ch_1", status="succeeded")
        place_order(order)

    charge.assert_called_once_with(order.pk, order.total)
    order.refresh_from_db()
    assert order.status == "paid"


@pytest.mark.django_db
def test_declined_card_leaves_order_unpaid(order):
    with patch("orders.services.charge_card", autospec=True) as charge:
        charge.side_effect = PaymentDeclined("insufficient funds")
        with pytest.raises(PaymentDeclined):
            place_order(order)

    order.refresh_from_db()
    assert order.status == "draft"        # the real assertion: nothing was committed


async def test_async_notify(monkeypatch):
    notify = AsyncMock(return_value=None)
    monkeypatch.setattr("orders.services.notify_partner", notify)
    await send_updates(order_ids=[1, 2])
    assert notify.await_count == 2


def test_reads_api_key_from_env(monkeypatch):
    monkeypatch.setenv("PARTNER_API_KEY", "test-key")
    assert build_client().api_key == "test-key"   # restored automatically
1–5
The patch target is `orders.services`, because that is where the name is looked up. Patching `billing.stripe.charge_card` would rebind a name this code no longer reads.
9
`assert_called_once_with` checks the arguments as well as the fact of the call — and `autospec=True` is what makes a wrong signature here fail rather than pass.
15–22
The failure path asserts on *state*, not on the mock: the order is still a draft. Asserting only that the mock raised would test the mock rather than the code.
25–29
`AsyncMock` because the collaborator is awaited; `await_count` rather than `call_count`, since a coroutine that is created but never awaited is a real bug this distinguishes.
32–34
`monkeypatch.setenv` is cleaner than `patch.dict(os.environ)` and undoes itself, so no test can leak an environment variable into the next.

Why this works: Only the boundary call is replaced. Everything else — the order, the transaction, the status transition — is real, so the tests still fail if the code stops doing its job.

Patching where the function is defined

Wrong

python
# orders/services.py:  from billing.stripe import charge_card
with patch("billing.stripe.charge_card") as charge:
    place_order(order)
charge.assert_called_once()      # AssertionError: not called — the real one ran

Better

python
with patch("orders.services.charge_card", autospec=True) as charge:
    place_order(order)
charge.assert_called_once()

What you see: Either the assertion fails saying the mock was never called, or — much worse — the test passes while the *real* function runs and charges a card in a sandbox nobody is watching.

Why: `from billing.stripe import charge_card` binds the function object into `orders.services` at import time. Patching `billing.stripe.charge_card` afterwards replaces the attribute on the *defining* module, but `orders.services` still holds its own reference to the original object. The name your code actually reads is `orders.services.charge_card`, so that is the one to replace.

Every part of a patch call, and what it decides

@patch("orders.services.charge_card", autospec=True, return_value="ch_1")

@patch

temporary, and self-undoing — Replaces the attribute for the duration of the test and restores it afterwards, whether the test passes, fails, or raises.

"orders.services

the module where the name is LOOKED UP — Not where the function is defined. `from billing import charge_card` copies the reference into orders.services, so that is the namespace to patch.

.charge_card"

the attribute being replaced — A string, resolved at call time — which is why a typo here silently patches nothing unless autospec catches it.

autospec=True

the guardrail — Builds the double from the real signature, so a wrong argument count or a nonexistent method raises instead of quietly returning another Mock.

return_value="ch_1"

what the call answers — Without it the call returns a Mock, which is truthy and often passes an assertion by accident. Use side_effect instead to raise or to vary per call.

  • Whole: @patch("orders.services.charge_card", autospec=True, return_value="ch_1")
  • @patch — temporary, and self-undoing: Replaces the attribute for the duration of the test and restores it afterwards, whether the test passes, fails, or raises.
  • "orders.services — the module where the name is LOOKED UP: Not where the function is defined. `from billing import charge_card` copies the reference into orders.services, so that is the namespace to patch.
  • .charge_card" — the attribute being replaced: A string, resolved at call time — which is why a typo here silently patches nothing unless autospec catches it.
  • autospec=True — the guardrail: Builds the double from the real signature, so a wrong argument count or a nonexistent method raises instead of quietly returning another Mock.
  • return_value="ch_1" — what the call answers: Without it the call returns a Mock, which is truthy and often passes an assertion by accident. Use side_effect instead to raise or to vary per call.

Which double, and when

Which double, and when
ClassAddsUse for
`Mock`attribute and call recordinga plain collaborator
`MagicMock`configured dunder methodsanything used with `len`, `in`, `for`, or `with`
`AsyncMock`calls return coroutinesanything you `await`
`patch(..., autospec=True)`the real signaturealways — it catches wrong calls
`monkeypatch`env vars, dict items, teardownsettings, `os.environ`, module-level dicts

Together

python
with patch("orders.services.charge_card", autospec=True) as charge:
    charge.return_value = "ch_1"
    place_order(order)
charge.assert_called_once_with(order.pk, order.total)

Remember: `Mock` says yes to everything, which is why a mistyped assertion silently passes — use `autospec=True` as the default so a wrong signature or a fabricated method raises. `MagicMock` adds dunders, `AsyncMock` returns coroutines and is counted with `await_count`. Patch where the name is **used**, because `from x import y` copies the reference into the importing module. And prefer `monkeypatch` for environment variables and dictionary entries, where its teardown is cleaner than `patch.dict`.

See also: the test double vocabulary · what to mock and what not to · pytest django and fixtures

Advertisement

The vocabulary of doubles

Stub, fake, spy and mock as a scale of how much a test depends on implementation.

Stubs, fakes, spies, and the test-double vocabulary

standardintermediate

"Test double" is the umbrella term for anything standing in for a real collaborator, and the varieties differ in how much they do. A **stub** returns canned answers and nothing more — it exists to make the code under test proceed. A **fake** is a working implementation that is unsuitable for production: an in-memory dictionary instead of Redis, a list instead of an outbox. A **spy** wraps the real thing and records how it was called, so behaviour is unchanged and you can still assert on the interaction. A **mock** is a double with expectations built in — it knows what it should be called with and fails if it is not. Python's `Mock` can play all four roles, which is why the vocabulary is worth keeping even though the class is the same.

Think of it as

The vocabulary is really a scale of how much you are willing to let the test depend on *how* the code works rather than *what* it produces. A fake sits at the safe end: it behaves like the real dependency, so the test asserts on outcomes and keeps working when the implementation changes. A mock with strict call expectations sits at the other end, asserting the exact sequence of calls — which is genuinely what you want when the interaction *is* the requirement ("we must charge the card exactly once"), and is a trap everywhere else, because a refactor that produces identical results now fails a test. Spies are the pragmatic middle when the real call is cheap and safe: let it happen, then check it happened. The practical guidance that falls out is to prefer the least specific double that can observe what you care about, and to reach for a fake whenever the dependency has a small, stable interface — a cache, a clock, a feature-flag store — because a good fake is written once and makes dozens of tests read like production code. Django ships two fakes you already use without naming them: `locmem` email, which is why `mail.outbox` works, and `LocMemCache`.

python
class FakeCache:                       # a fake: small, real, in-memory
    def __init__(self): self._d = {}
    def get(self, k, default=None): return self._d.get(k, default)
    def set(self, k, v, timeout=None): self._d[k] = v

What we're doing: One dependency, three doubles — each chosen for what the test needs to observe.

orders/tests/test_doubles.pypython
# STUB — the exchange rate just has to be *a* number.
@pytest.mark.django_db
def test_total_converts_currency(order):
    with patch("orders.services.fetch_rate", return_value=Decimal("1.25")):
        assert order.total_in("USD") == order.total * Decimal("1.25")


# FAKE — a working cache, so the test exercises real caching behaviour.
class FakeCache:
    def __init__(self):
        self._d = {}

    def get(self, key, default=None):
        return self._d.get(key, default)

    def set(self, key, value, timeout=None):
        self._d[key] = value


@pytest.mark.django_db
def test_second_call_is_served_from_cache(order, monkeypatch):
    cache = FakeCache()
    monkeypatch.setattr("orders.services.cache", cache)
    price_for(order)                       # miss: computes and stores
    with patch("orders.services.compute_price") as compute:
        price_for(order)                   # hit: must not compute again
    compute.assert_not_called()


# SPY — the real recalculation runs; we also assert it was invoked once.
@pytest.mark.django_db
def test_placing_an_order_recalculates_once(order):
    with patch("orders.services.recalculate", wraps=recalculate) as spy:
        place_order(order)
    order.refresh_from_db()
    assert order.total == Decimal("48.00")   # the real work happened
    spy.assert_called_once()                 # and exactly once
4
A stub, because the test is about the arithmetic and the rate is an input. Nothing is asserted about how `fetch_rate` was called — that is not the subject.
9–17
Twenty lines of fake, reused by every caching test in the file. A `Mock` here would make the test assert on `cache.get` calls rather than on whether caching actually works.
25–27
The assertion is a *negative* one about the real behaviour: on the second call, nothing recomputed. That is only observable because the fake genuinely stored the value.
33–37
A spy: `wraps=` lets the real function run, so the total is genuinely correct, and the recorded call also proves it happened exactly once.

Why this works: Three different questions — what does the arithmetic produce, does caching work, was the real work done once — need three different doubles, and using a strict mock for all three would test the implementation instead of any of them.

Four doubles, and what each one lets a test see

Stub and Fake

Stub: canned answers

just enough for the code to proceed

Fake: a working simplified version

an in-memory dict, LocMemCache, locmem email

Survives refactors

the test is about results, not about calls

Spy

patch(..., wraps=real)

the real call still happens

Assert on both

the outcome AND that it was called

Only when the real call is safe

cheap, deterministic, no side effect you mind

Mock

Right when the call is the requirement

"charge the card exactly once"

Wrong everywhere else

a refactor with identical results fails the test

Use the least specific double that works

reach for mock last, not first

  • A collaborator you do not want to call for real
  • Stub and Fake — assert on the outcome
    • Stub: canned answers — just enough for the code to proceed
    • Fake: a working simplified version — an in-memory dict, LocMemCache, locmem email
    • Survives refactors — the test is about results, not about calls
  • Spy — real behaviour, plus a record
    • patch(..., wraps=real) — the real call still happens
    • Assert on both — the outcome AND that it was called
    • Only when the real call is safe — cheap, deterministic, no side effect you mind
  • Mock — the interaction IS the assertion
    • Right when the call is the requirement — "charge the card exactly once"
    • Wrong everywhere else — a refactor with identical results fails the test
    • Use the least specific double that works — reach for mock last, not first

The four doubles, by how much they constrain the test

The four doubles, by how much they constrain the test
DoubleBehaviourAssertion is aboutBreaks on
Stubcanned answersthe outcomealmost nothing
Fakea real, simplified implementationthe outcomea genuine behaviour change
Spythe real thing, recordedthe outcome and the calla real behaviour change
Mockcanned answers + expectationsthe interactionany refactor of how it is called

Together

python
# spy: real behaviour, plus a record of the call
with patch("orders.services.recalculate", wraps=recalculate) as spy:
    place_order(order)
spy.assert_called_once()

Remember: Stub returns canned answers, fake is a working simplified implementation, spy wraps the real thing and records, mock carries expectations. They form a scale of how much the test depends on *how* the code works: fakes and stubs assert on outcomes and survive refactors, while a strict mock asserts the interaction and is right only when the interaction is the requirement. Reach for the least specific double that can observe what you care about — and remember Django already ships two fakes you rely on, `locmem` email and `LocMemCache`.

See also: mock magicmock patch and asyncmock · what to mock and what not to · cache backends keys and ttl

Advertisement

Where the boundary belongs

What to mock, what never to mock, and why a fully mocked test proves almost nothing.

What to mock, what not to, and behaviour over implementation

coreadvanced

Mock at the boundaries of your system and nowhere else. **Mock**: anything that leaves the process (a payment provider, an SMTP server, an SMS gateway), anything nondeterministic (the clock, `random`, `uuid4`), and anything slow or expensive that you do not own. **Do not mock**: your own ORM, your own models, your own service functions, or the database — those are the things the test exists to exercise, and replacing them means the test checks that your mocks were configured correctly. And mock *behaviour*, not implementation: assert that a charge was made, not that a particular private helper was called in a particular order, or a refactor that changes nothing observable will fail.

Think of it as

The section states the failure directly: a test that passes only because every dependency is mocked may provide very little confidence. The reason is that a mock encodes your *belief* about how a collaborator behaves, so a suite of heavily mocked tests verifies that your code matches your beliefs — and if a belief is wrong, every test still passes and production still breaks. That gives a simple rule: the more of the real system a test runs, the more it is worth, so mock the minimum that makes the test fast, deterministic and safe. Concretely, that boundary is almost always "leaves the process or is not deterministic". Everything inside — the ORM, your services, the transaction — should be real, which is precisely why Django gives you a fast test database instead of encouraging you to mock the ORM. The second half is about *what you assert*. Asserting on the call sequence of internal functions couples the test to today's implementation, so a refactor that produces identical results turns the suite red and teaches people that refactoring is expensive. Assert on observable outcomes — a row changed, a response returned, a message queued — and reserve interaction assertions for cases where the interaction genuinely is the requirement, which in practice is mostly "we must call the payment provider exactly once".

python
with freeze_time("2026-09-04T10:00:00Z"), \
     patch("orders.services.charge_card", autospec=True):
    place_order(order)          # everything inside the boundary stays real

What we're doing: The same behaviour tested twice — once mocked to death, once at the boundary only.

orders/tests/test_place_order.pypython
# ---- Mocked to death: passes even if place_order is completely broken ----
@pytest.mark.django_db
def test_place_order_bad(order):
    with patch("orders.services.validate_stock") as validate, \
         patch("orders.services.charge_card") as charge, \
         patch("orders.services.Order.objects") as objects:
        place_order(order)

    validate.assert_called_once()
    charge.assert_called_once()
    objects.filter.assert_called_once()      # asserts the implementation, not the result


# ---- Boundary only: the ORM, the transaction and the signals are all real ----
@pytest.mark.django_db
def test_place_order_good(order, settings, django_capture_on_commit_callbacks):
    settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

    with freeze_time("2026-09-04T10:00:00Z"), \
         patch("orders.services.charge_card", autospec=True) as charge:
        charge.return_value = Charge(id="ch_1", status="succeeded")
        with django_capture_on_commit_callbacks(execute=True):
            place_order(order)

    order.refresh_from_db()
    assert order.status == "paid"
    assert order.paid_at == datetime(2026, 9, 4, 10, 0, tzinfo=timezone.utc)
    assert Payment.objects.filter(order=order, charge_id="ch_1").exists()
    assert len(mail.outbox) == 1
    charge.assert_called_once_with(order.pk, order.total)   # the one interaction that IS the requirement
4–6
Three patches, one of them replacing the ORM manager itself. After this the test cannot observe anything real — delete the body of `place_order` and it still passes.
11
Asserting that `objects.filter` was called is an assertion about today's implementation. Rewriting the query with identical results fails this test.
20
One patch, at the only line that leaves the process. Everything else — the transaction, the status transition, the signal, the outbox — runs for real.
25–29
Four assertions about observable state, none about internal calls. Any of them fails if `place_order` stops doing its job, and none fails on a refactor.
30
The single interaction assertion, kept because "charge exactly once, with this amount" genuinely is the requirement rather than an implementation detail.

Why this works: The second test would catch a broken transaction, a wrong status, a missing payment row, a missing email and a wrong timestamp. The first would catch none of them, and both are green today.

Mocking the ORM

Wrong

python
with patch("orders.services.Order.objects") as objects:
    objects.filter.return_value.first.return_value = order
    place_order(order)
objects.filter.assert_called_once_with(pk=order.pk)

Better

python
@pytest.mark.django_db
def test_place_order(order):
    place_order(order)
    order.refresh_from_db()
    assert order.status == "paid"

What you see: The test passes while the real query is wrong — a missing filter, a bad `select_related`, an `update()` that matches no rows. It also fails whenever the query is rewritten, so it objects to correct changes and permits incorrect ones.

Why: Mocking the ORM replaces the thing most likely to be wrong with an object that agrees with whatever you wrote. Query construction is exactly where Django bugs live — wrong lookup, wrong manager, forgotten filter — and a mock cannot detect any of them because it never touches a database. Django provides a fast test database with rollback isolation precisely so this is unnecessary.

Mock by distance from your code, not by convenience
Your models and ORM
never mock — this is the thing under test
Your service functions
never mock — mocking them tests your mock setup
The test database
real, with rollback isolation. Fast enough and far more truthful.
Email / cache
use Django's shipped fakes: locmem and LocMemCache
timezone.now(), uuid4()
freeze it — a test must not fail on a Tuesday
Payment provider, SMS, S3
mock at this boundary and nowhere closer in
  • Your models and ORM: your code owns it, deterministic and fast — never mock — this is the thing under test
  • Your service functions: your code owns it, deterministic and fast — never mock — mocking them tests your mock setup
  • The test database: your code owns it, between deterministic and fast and slow, costly, or nondeterministic — real, with rollback isolation. Fast enough and far more truthful.
  • Email / cache: between your code owns it and someone else owns it, between deterministic and fast and slow, costly, or nondeterministic — use Django's shipped fakes: locmem and LocMemCache
  • timezone.now(), uuid4(): between your code owns it and someone else owns it, slow, costly, or nondeterministic — freeze it — a test must not fail on a Tuesday
  • Payment provider, SMS, S3: someone else owns it, slow, costly, or nondeterministic — mock at this boundary and nowhere closer in

Mock it, or not?

Mock it, or not?
DependencyMock?Why
A payment provider, an SMS gatewayyesleaves the process; costs money; not yours
`timezone.now()`, `random`, `uuid4()`yesnondeterministic — freeze it instead of hoping
An email backendno — use the fakeDjango ships `locmem`; `mail.outbox` is better than a mock
The cacheno — use `LocMemCache`a real backend, and the test then proves caching works
Your ORM / models**no**this is what the test is for
Your own service functions**no**mocking them tests your mock configuration
The database**no**the test database is fast and truthful

Together

python
with patch("orders.services.charge_card", autospec=True) as charge:   # boundary
    place_order(order)          # everything else — ORM, transaction, signals — is real
assert Order.objects.get(pk=order.pk).status == "paid"

Remember: Mock at the boundary: anything leaving the process, and anything nondeterministic. Never mock your own ORM, models, services or the database — those are the things the test exists to exercise, and replacing them means the test verifies your mock configuration instead. Assert on observable outcomes rather than on which internal helpers were called, so a behaviour-preserving refactor stays green. And take the section's warning literally: a test that passes only because everything is mocked provides very little confidence.

See also: the test double vocabulary · mock magicmock patch and asyncmock · the test case classes · testing side effects idempotency and integrations

Advertisement