Isolation & Contracts

Mock vs MagicMock vs AsyncMock — When to Use Each

Every Python test suite eventually hits the question of which test double to instantiate, and the wrong choice produces one of three signature failures: a bare Mock blowing up with AttributeError: __enter__ inside a with block, a MagicMock raising TypeError: object MagicMock can't be used in 'await' expression, or an AsyncMock silently wrapping a synchronous call in a coroutine nobody awaits. The three classes form a deliberate hierarchy — MagicMock subclasses Mock, and AsyncMock (added in Python 3.8) subclasses MagicMock — each adding support for one more protocol. This guide gives a decision procedure tied to the exact protocol the code under test relies on, so you pick the class that prevents the failure rather than discovering it in CI.

Prerequisites

  • Python 3.8+ for AsyncMock; on 3.7 and earlier you hand-roll awaitable doubles. Examples target 3.11.
  • unittest.mock from the standard library.
  • Working knowledge of context managers, the iterator protocol, and async/await. The Deep Dive into unittest.mock covers the shared internals.
Mock, MagicMock and AsyncMock feature matrix A comparison matrix showing which capabilities Mock, MagicMock and AsyncMock support across call and attribute tracking, dunder protocols, awaitable returns, and which Python version each is available in. Mock supports tracking only; MagicMock adds dunder protocols; AsyncMock adds awaitable returns and requires Python 3.8 or newer. Which double supports what Mock MagicMock AsyncMock call & attr tracking yes yes yes dunder protocols no yes yes awaitable on call no no yes available since always always 3.8+ Each class adds one protocol layer to its parent — pick the lowest that fits.
Mock tracks calls; MagicMock adds dunder protocol support; AsyncMock (3.8+) adds awaitable returns. Pick the lowest layer that covers what the code under test exercises.

Solution

The decision is mechanical: match the class to the protocol the code exercises. Use the lowest layer that still works.

Walk it in one direction only. Does the code under test await the double, or does it call an async def on the dependency? If yes, you need AsyncMock. If no, does it drive the double through a dunder protocol — with, async with, for, in, len(), [], or an operator? If yes, you need MagicMock. Otherwise a Mock with a spec is the leanest correct choice. The one shortcut that collapses all three questions is to derive the double from the real object with autospec and let it pick per attribute.

Decision flowchart for choosing a test double class A flowchart. Start at "Which test double?". First ask if the code under test awaits the double or calls an async def: if yes, use AsyncMock. If no, ask if it drives a dunder protocol such as with, for, len or square-bracket indexing: if yes, use MagicMock; if no, use Mock with a spec. A shortcut box notes that create_autospec or autospec equals True selects the right class per attribute and skips the whole tree. Which test double? Awaits it, or calls an async def on it? yes AsyncMock await mock() resolves no Drives a dunder protocol? with · for · in · len() · [ ] yes MagicMock dunders pre-wired no Mock(spec=Target) leanest correct choice Shortcut — skips the whole tree create_autospec / autospec=True picks the right class per attribute
One-way walk: await first, then dunder protocols, then a plain spec'd Mock. Autospec collapses the whole decision by choosing the correct class for each attribute against the real object.
Python
import asyncio
from unittest.mock import Mock, MagicMock, AsyncMock, create_autospec


# --- 1. Mock: plain calls and attributes. Add spec= for a strict contract. ---
class Repo:
    def get(self, key: str) -> str: ...

repo = Mock(spec=Repo)              # AttributeError on anything not on Repo
repo.get.return_value = "row"
assert repo.get("k") == "row"
repo.get.assert_called_once_with("k")


# --- 2. MagicMock: needed for dunder protocols (with / for / len / []). ---
conn = MagicMock()                  # __enter__/__exit__ pre-wired
with conn as session:               # a bare Mock() raises AttributeError here
    session.execute("SELECT 1")
conn.__enter__.assert_called_once()

items = MagicMock()
items.__iter__.return_value = iter([1, 2, 3])
assert list(items) == [1, 2, 3]     # iteration works because __iter__ exists


# --- 3. AsyncMock: required when the double is awaited (Python 3.8+). ---
class Client:
    async def fetch(self, url: str) -> dict: ...

async def use(client: Client) -> dict:
    return await client.fetch("/data")   # the code under test awaits here

async def main() -> None:
    client = AsyncMock(spec=Client)       # call returns an awaitable
    client.fetch.return_value = {"ok": True}
    result = await use(client)            # MagicMock here -> TypeError on await
    assert result == {"ok": True}
    client.fetch.assert_awaited_once_with("/data")   # await-aware assertion

asyncio.run(main())


# --- 4. Let autospec choose per attribute: sync stays Mock/MagicMock,
#        coroutine functions become AsyncMock automatically. ---
class Mixed:
    def sync_op(self) -> int: ...
    async def async_op(self) -> int: ...

m = create_autospec(Mixed)
assert not asyncio.iscoroutinefunction(m.sync_op)
assert asyncio.iscoroutinefunction(m.async_op)   # picked AsyncMock for you

Why this works

The three classes are a subclass chain where each adds capability: Mock records calls and auto-creates child attributes, MagicMock additionally pre-configures the magic methods that protocols like with, for, and len() invoke, and AsyncMock overrides __call__ to return a coroutine so await mock() resolves and assert_awaited* helpers track it. MagicMock does not configure every dunder — it deliberately skips a handful (__del__, __getattr__, __setattr__, and the pickling hooks) whose auto-configuration would break normal object behaviour — but every protocol dunder you would exercise in a test is present. In Python 3.8+ MagicMock also detects the async magic methods: __aenter__, __aexit__, __aiter__, and __anext__ are backed by AsyncMock instances, so async with and async for work on a plain MagicMock even though a direct call does not return an awaitable.

Because autospec strict mocking inspects each attribute with asyncio.iscoroutinefunction, create_autospec and autospec=True substitute the correct class per member against the real signature — which is why deriving the double from the real object is the safest default in mixed sync/async code. It removes the choice entirely: you never name Mock, MagicMock, or AsyncMock, and the double stays correct when the dependency later turns a def into an async def.

Edge cases and failure modes

  • Awaiting a MagicMock raises TypeError. await magic_mock() fails because the call returns a plain MagicMock, not an awaitable. Switch the double — or the specific attribute — to AsyncMock.
  • AsyncMock for a sync function over-wraps the call. Calling a sync dependency backed by AsyncMock returns an unawaited coroutine; the real call site never awaits it, so the value is lost and you get a "coroutine was never awaited" warning. Mirror the real signature, ideally via autospec. This overlaps with debugging tracing "coroutine was never awaited" warnings.
  • A bare Mock cannot enter a with block or be iterated. It has no __enter__/__iter__. Use MagicMock, or manually attach the dunder methods if you specifically want Mock's leaner surface.
  • async with and async for need the async dunders, not AsyncMock on the object itself. An async context manager is entered via __aenter__/__aexit__, and an async iterator via __aiter__/__anext__. A plain MagicMock already backs those four with AsyncMock, so async with pool.acquire() as conn: works if acquire() returns a MagicMock. Configure the entered value with cm.__aenter__.return_value = conn. Reaching for AsyncMock on the object gives you an awaitable call but still no __aenter__ unless you spec against the real async context manager.
  • return_value is shared across every call. All three classes replay one configured return_value for every invocation regardless of arguments. When a test needs per-call or per-argument behaviour, move to side_effect with a list or callable; the interaction between the two is where subtle failures hide, covered in resolving side_effect and return_value conflicts.
  • spec does not change the call's await behaviour. Mock(spec=AsyncClient) still returns sync values; spec constrains the attribute surface but not the call protocol. For awaitables you need AsyncMock (or autospec, which selects it).
  • Mixing assertion families. assert_called_* and assert_awaited_* are not interchangeable: on an AsyncMock, assert_called_once passes once the coroutine is created, while assert_awaited_once requires it to have been awaited. Choose the await-aware variant for coroutine doubles: a coroutine that is created but never awaited passes assert_called and fails assert_awaited, which is exactly the signal you want when a caller forgets to await.

Picking the class from the call site

You rarely have to reason about the class hierarchy in the abstract: the call site tells you which one you need. Three questions, asked in order, resolve every case.

Does the code await the collaborator, or call something that returns an awaitable? If yes, the double must be an AsyncMock, or patch must be told to make one — since Python 3.8, patch and create_autospec both detect async def on the target and build the async class automatically, which is why an explicitly constructed Mock() passed as an override is the usual cause of an un-awaited coroutine warning.

Does the code use the object through a dunder protocol — len(), in, with, [], iteration, arithmetic? If yes, you need MagicMock (or autospec, which builds one). A plain Mock raises TypeError on the first protocol use.

Otherwise use Mock, and prefer create_autospec whenever the stand-in has a real class behind it.

Three questions that pick the mock class A decision diagram: the question is whether the call site awaits the collaborator, uses a dunder protocol, or does neither. Awaiting selects AsyncMock, dunder use selects MagicMock, and everything else selects Mock, with a note that create_autospec is preferred whenever a real class exists to bind to. Three questions that pick the mock class How does the call site use the collaborator? it is awaited AsyncMock await returns the value assert_awaited_once dunder protocol MagicMock len, with, in, [] protocols preconfigured plain calls only Mock unknown protocol raises strictest default If a real class exists, create_autospec picks the right base for you and enforces signatures.
Read the call site, not the implementation: the protocol the test exercises decides the class.

In review, state the reason for the class in a comment when it is not obvious from the call site — a future reader cannot tell an intentional MagicMock from a careless one, and the difference decides whether a protocol change fails the suite.

The rule holds for nested doubles too. A MagicMock returns child MagicMocks, and an AsyncMock returns child AsyncMocks only for attributes that were async def on the spec — an unspecced AsyncMock makes every child awaitable, which quietly turns a synchronous helper into a coroutine and moves the failure to a confusing place.

Frequently Asked Questions

When must I use AsyncMock instead of MagicMock? Use AsyncMock whenever the code under test awaits the double. AsyncMock returns an awaitable on call so await mock() resolves; a MagicMock returns a plain value, so awaiting it raises TypeError: object MagicMock can't be used in await expression.

Why does a bare Mock fail inside a with statement? A with statement calls enter and exit, which Mock does not pre-configure. MagicMock pre-wires the common dunder methods, so it works in with, for, and len contexts without manual setup.

Does autospec pick the right mock class automatically? Yes. create_autospec and patch with autospec=True inspect each attribute and substitute AsyncMock for coroutine functions and MagicMock or Mock for the rest, so awaitables and dunder protocols are handled to match the real object.

← Back to Deep Dive into unittest.mock