AsyncMock limitations with nested asynchronous context managers
0 reputation · 16 May 2023, 22:53 UTC
0 reputation · 16 May 2023, 22:53 UTC
When testing asynchronous service integrations in Python 3.8+, unittest.mock.AsyncMock is commonly used to simulate external API clients without production credentials. While AsyncMock handles standard async def functions, complexity arises when the integration relies on nested asynchronous context managers.
Specifically, if a client method returns an async context manager that itself yields another (for example, a session wrapping a transaction), the propagation of exceptions and state through the mock can be inconsistent. There is uncertainty about how AsyncMock handles the __aexit__ method in deeply nested structures, and whether behavior differs across Python versions 3.8 through 3.12.
How does AsyncMock behave when an exception is raised during the __aexit__ phase of a nested async context manager? Is there a reliable pattern to ensure the mock propagates these errors back to the caller without manually defining the entire return chain?
32275 reputation · 17 May 2023, 09:31 UTC
When an exception bubbles out of an async with block, the interpreter calls the context manager’s __aexit__ with the exception info. AsyncMock implements __aenter__ and __aexit__ as async callables that return self and an awaitable yielding None, respectively. This means a single AsyncMock can stand in for a regular async context manager, but it does not automatically create a distinct chain of objects for nested contexts.
What happens in a nested situation?
AsyncMock instance.__aenter__ returns the inner mock, which in turn has its own __aenter__ and __aexit__.__aexit__ raises, that exception is passed to the outer __aexit__ in LIFO order, just like a real context manager.__aexit__ returns None (treated as False), any exception that reaches an __aexit__ is re‑raised unless the mock’s __aexit__.return_value is explicitly set to True.Confirmed facts across Python 3.8–3.12
AsyncMock.__aexit__ is identical in all these releases; only type hints were tightened in 3.11.__context__, __cause__) is preserved by the interpreter, but AsyncMock itself does not synthesize the implicit AsyncExitStack logic.mock.__aexit__.return_value = True suppresses the exception, which can mask bugs if done unintentionally.To guarantee correct propagation without writing an entire return chain by hand, follow this minimal template:
from unittest.mock import AsyncMock
# Outer mock – e.g. a client that returns a session
client = AsyncMock(spec=RealClient)
# Inner mock – e.g. a session that returns a transaction
session = AsyncMock(spec=RealSession)
transaction = AsyncMock(spec=RealTransaction)
# Wire the chain
client.__aenter__.return_value = session
client.__aexit__.return_value = None # default
session.__aenter__.return_value = transaction
session.__aexit__.return_value = None
transaction.__aenter__.return_value = transaction
transaction.__aexit__.return_value = None
# Example test
async def test_nested():
with pytest.raises(ValueError):
async with client as sess:
async with sess as trans:
raise ValueError("boom") # triggers inner __aexit__
# Verify call order
assert session.__aexit__.call_args_list[0][0][0] is ValueError
assert client.__aexit__.call_args_list[0][0][0] is ValueError
This pattern keeps the mock hierarchy explicit, ensures each level’s __aexit__ receives the correct exception, and avoids accidental suppression.
If your test still behaves unexpectedly, confirm that each context level is a distinct AsyncMock instance and that you haven’t overridden __aexit__.return_value globally. Also, verify that the exception raised inside the innermost block is the one you expect (type, message).
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.