Skip to content

Commit 865bb68

Browse files
lisroachmatrixise
authored andcommitted
[3.8] bpo-38093: Correctly returns AsyncMock for async subclasses. (GH-15947) (GH-16299)
(cherry picked from commit 8b03f94) Co-authored-by: Lisa Roach <lisaroach14@gmail.com>
1 parent f4e0ceb commit 865bb68

5 files changed

Lines changed: 180 additions & 77 deletions

File tree

Doc/library/unittest.mock-examples.rst

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import asyncio
1616
import unittest
17-
from unittest.mock import Mock, MagicMock, patch, call, sentinel
17+
from unittest.mock import Mock, MagicMock, AsyncMock, patch, call, sentinel
1818

1919
class SomeClass:
2020
attribute = 'this is a doctest'
@@ -280,39 +280,42 @@ function returns is what the call returns:
280280
Mocking asynchronous iterators
281281
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
282282

283-
Since Python 3.8, ``MagicMock`` has support to mock :ref:`async-iterators`
284-
through ``__aiter__``. The :attr:`~Mock.return_value` attribute of ``__aiter__``
285-
can be used to set the return values to be used for iteration.
283+
Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock
284+
:ref:`async-iterators` through ``__aiter__``. The :attr:`~Mock.return_value`
285+
attribute of ``__aiter__`` can be used to set the return values to be used for
286+
iteration.
286287

287-
>>> mock = MagicMock()
288+
>>> mock = MagicMock() # AsyncMock also works here
288289
>>> mock.__aiter__.return_value = [1, 2, 3]
289290
>>> async def main():
290291
... return [i async for i in mock]
292+
...
291293
>>> asyncio.run(main())
292294
[1, 2, 3]
293295

294296

295297
Mocking asynchronous context manager
296298
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
297299

298-
Since Python 3.8, ``MagicMock`` has support to mock
299-
:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. The
300-
return value of ``__aenter__`` is an :class:`AsyncMock`.
300+
Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock
301+
:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``.
302+
By default, ``__aenter__`` and ``__aexit__`` are ``AsyncMock`` instances that
303+
return an async function.
301304

302305
>>> class AsyncContextManager:
303-
...
304306
... async def __aenter__(self):
305307
... return self
306-
...
307-
... async def __aexit__(self):
308+
... async def __aexit__(self, exc_type, exc, tb):
308309
... pass
309-
>>> mock_instance = MagicMock(AsyncContextManager())
310+
...
311+
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
310312
>>> async def main():
311313
... async with mock_instance as result:
312314
... pass
315+
...
313316
>>> asyncio.run(main())
314-
>>> mock_instance.__aenter__.assert_called_once()
315-
>>> mock_instance.__aexit__.assert_called_once()
317+
>>> mock_instance.__aenter__.assert_awaited_once()
318+
>>> mock_instance.__aexit__.assert_awaited_once()
316319

317320

318321
Creating a Mock from an Existing Object

Lib/unittest/mock.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -988,9 +988,13 @@ def _get_child_mock(self, /, **kw):
988988
_type = type(self)
989989
if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
990990
klass = AsyncMock
991-
if issubclass(_type, AsyncMockMixin):
991+
elif _new_name in _sync_async_magics:
992+
# Special case these ones b/c users will assume they are async,
993+
# but they are actually sync (ie. __aiter__)
992994
klass = MagicMock
993-
if not issubclass(_type, CallableMixin):
995+
elif issubclass(_type, AsyncMockMixin):
996+
klass = AsyncMock
997+
elif not issubclass(_type, CallableMixin):
994998
if issubclass(_type, NonCallableMagicMock):
995999
klass = MagicMock
9961000
elif issubclass(_type, NonCallableMock) :
@@ -1867,7 +1871,7 @@ def _patch_stopall():
18671871
'__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__',
18681872
'__getstate__', '__setstate__', '__getformat__', '__setformat__',
18691873
'__repr__', '__dir__', '__subclasses__', '__format__',
1870-
'__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__',
1874+
'__getnewargs_ex__',
18711875
}
18721876

18731877

@@ -1886,10 +1890,12 @@ def method(self, /, *args, **kw):
18861890

18871891
# Magic methods used for async `with` statements
18881892
_async_method_magics = {"__aenter__", "__aexit__", "__anext__"}
1889-
# `__aiter__` is a plain function but used with async calls
1890-
_async_magics = _async_method_magics | {"__aiter__"}
1893+
# Magic methods that are only used with async calls but are synchronous functions themselves
1894+
_sync_async_magics = {"__aiter__"}
1895+
_async_magics = _async_method_magics | _sync_async_magics
18911896

1892-
_all_magics = _magics | _non_defaults
1897+
_all_sync_magics = _magics | _non_defaults
1898+
_all_magics = _all_sync_magics | _async_magics
18931899

18941900
_unsupported_magics = {
18951901
'__getattr__', '__setattr__',

Lib/unittest/test/testmock/testasync.py

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -377,43 +377,88 @@ def test_add_side_effect_iterable(self):
377377

378378
class AsyncContextManagerTest(unittest.TestCase):
379379
class WithAsyncContextManager:
380-
def __init__(self):
381-
self.entered = False
382-
self.exited = False
383-
384380
async def __aenter__(self, *args, **kwargs):
385381
self.entered = True
386382
return self
387383

388384
async def __aexit__(self, *args, **kwargs):
389385
self.exited = True
390386

391-
def test_magic_methods_are_async_mocks(self):
392-
mock = MagicMock(self.WithAsyncContextManager())
393-
self.assertIsInstance(mock.__aenter__, AsyncMock)
394-
self.assertIsInstance(mock.__aexit__, AsyncMock)
387+
class WithSyncContextManager:
388+
def __enter__(self, *args, **kwargs):
389+
return self
390+
391+
def __exit__(self, *args, **kwargs):
392+
pass
393+
394+
class ProductionCode:
395+
# Example real-world(ish) code
396+
def __init__(self):
397+
self.session = None
398+
399+
async def main(self):
400+
async with self.session.post('https://python.org') as response:
401+
val = await response.json()
402+
return val
403+
404+
def test_async_magic_methods_are_async_mocks_with_magicmock(self):
405+
cm_mock = MagicMock(self.WithAsyncContextManager())
406+
self.assertIsInstance(cm_mock.__aenter__, AsyncMock)
407+
self.assertIsInstance(cm_mock.__aexit__, AsyncMock)
408+
409+
def test_magicmock_has_async_magic_methods(self):
410+
cm = MagicMock(name='magic_cm')
411+
self.assertTrue(hasattr(cm, "__aenter__"))
412+
self.assertTrue(hasattr(cm, "__aexit__"))
413+
414+
def test_magic_methods_are_async_functions(self):
415+
cm = MagicMock(name='magic_cm')
416+
self.assertIsInstance(cm.__aenter__, AsyncMock)
417+
self.assertIsInstance(cm.__aexit__, AsyncMock)
418+
# AsyncMocks are also coroutine functions
419+
self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__))
420+
self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__))
421+
422+
def test_set_return_value_of_aenter(self):
423+
def inner_test(mock_type):
424+
pc = self.ProductionCode()
425+
pc.session = MagicMock(name='sessionmock')
426+
cm = mock_type(name='magic_cm')
427+
response = AsyncMock(name='response')
428+
response.json = AsyncMock(return_value={'json': 123})
429+
cm.__aenter__.return_value = response
430+
pc.session.post.return_value = cm
431+
result = asyncio.run(pc.main())
432+
self.assertEqual(result, {'json': 123})
433+
434+
for mock_type in [AsyncMock, MagicMock]:
435+
with self.subTest(f"test set return value of aenter with {mock_type}"):
436+
inner_test(mock_type)
395437

396438
def test_mock_supports_async_context_manager(self):
397-
called = False
398-
instance = self.WithAsyncContextManager()
399-
mock_instance = MagicMock(instance)
439+
def inner_test(mock_type):
440+
called = False
441+
cm = self.WithAsyncContextManager()
442+
cm_mock = mock_type(cm)
443+
444+
async def use_context_manager():
445+
nonlocal called
446+
async with cm_mock as result:
447+
called = True
448+
return result
400449

401-
async def use_context_manager():
402-
nonlocal called
403-
async with mock_instance as result:
404-
called = True
405-
return result
406-
407-
result = asyncio.run(use_context_manager())
408-
self.assertFalse(instance.entered)
409-
self.assertFalse(instance.exited)
410-
self.assertTrue(called)
411-
self.assertTrue(mock_instance.entered)
412-
self.assertTrue(mock_instance.exited)
413-
self.assertTrue(mock_instance.__aenter__.called)
414-
self.assertTrue(mock_instance.__aexit__.called)
415-
self.assertIsNot(mock_instance, result)
416-
self.assertIsInstance(result, AsyncMock)
450+
cm_result = asyncio.run(use_context_manager())
451+
self.assertTrue(called)
452+
self.assertTrue(cm_mock.__aenter__.called)
453+
self.assertTrue(cm_mock.__aexit__.called)
454+
cm_mock.__aenter__.assert_awaited()
455+
cm_mock.__aexit__.assert_awaited()
456+
# We mock __aenter__ so it does not return self
457+
self.assertIsNot(cm_mock, cm_result)
458+
459+
for mock_type in [AsyncMock, MagicMock]:
460+
with self.subTest(f"test context manager magics with {mock_type}"):
461+
inner_test(mock_type)
417462

418463
def test_mock_customize_async_context_manager(self):
419464
instance = self.WithAsyncContextManager()
@@ -481,27 +526,30 @@ async def __anext__(self):
481526

482527
raise StopAsyncIteration
483528

484-
def test_mock_aiter_and_anext(self):
485-
instance = self.WithAsyncIterator()
486-
mock_instance = MagicMock(instance)
487-
488-
self.assertEqual(asyncio.iscoroutine(instance.__aiter__),
489-
asyncio.iscoroutine(mock_instance.__aiter__))
490-
self.assertEqual(asyncio.iscoroutine(instance.__anext__),
491-
asyncio.iscoroutine(mock_instance.__anext__))
492-
493-
iterator = instance.__aiter__()
494-
if asyncio.iscoroutine(iterator):
495-
iterator = asyncio.run(iterator)
496-
497-
mock_iterator = mock_instance.__aiter__()
498-
if asyncio.iscoroutine(mock_iterator):
499-
mock_iterator = asyncio.run(mock_iterator)
529+
def test_aiter_set_return_value(self):
530+
mock_iter = AsyncMock(name="tester")
531+
mock_iter.__aiter__.return_value = [1, 2, 3]
532+
async def main():
533+
return [i async for i in mock_iter]
534+
result = asyncio.run(main())
535+
self.assertEqual(result, [1, 2, 3])
536+
537+
def test_mock_aiter_and_anext_asyncmock(self):
538+
def inner_test(mock_type):
539+
instance = self.WithAsyncIterator()
540+
mock_instance = mock_type(instance)
541+
# Check that the mock and the real thing bahave the same
542+
# __aiter__ is not actually async, so not a coroutinefunction
543+
self.assertFalse(asyncio.iscoroutinefunction(instance.__aiter__))
544+
self.assertFalse(asyncio.iscoroutinefunction(mock_instance.__aiter__))
545+
# __anext__ is async
546+
self.assertTrue(asyncio.iscoroutinefunction(instance.__anext__))
547+
self.assertTrue(asyncio.iscoroutinefunction(mock_instance.__anext__))
548+
549+
for mock_type in [AsyncMock, MagicMock]:
550+
with self.subTest(f"test aiter and anext corourtine with {mock_type}"):
551+
inner_test(mock_type)
500552

501-
self.assertEqual(asyncio.iscoroutine(iterator.__aiter__),
502-
asyncio.iscoroutine(mock_iterator.__aiter__))
503-
self.assertEqual(asyncio.iscoroutine(iterator.__anext__),
504-
asyncio.iscoroutine(mock_iterator.__anext__))
505553

506554
def test_mock_async_for(self):
507555
async def iterate(iterator):
@@ -512,19 +560,30 @@ async def iterate(iterator):
512560
return accumulator
513561

514562
expected = ["FOO", "BAR", "BAZ"]
515-
with self.subTest("iterate through default value"):
516-
mock_instance = MagicMock(self.WithAsyncIterator())
517-
self.assertEqual([], asyncio.run(iterate(mock_instance)))
563+
def test_default(mock_type):
564+
mock_instance = mock_type(self.WithAsyncIterator())
565+
self.assertEqual(asyncio.run(iterate(mock_instance)), [])
566+
518567

519-
with self.subTest("iterate through set return_value"):
520-
mock_instance = MagicMock(self.WithAsyncIterator())
568+
def test_set_return_value(mock_type):
569+
mock_instance = mock_type(self.WithAsyncIterator())
521570
mock_instance.__aiter__.return_value = expected[:]
522-
self.assertEqual(expected, asyncio.run(iterate(mock_instance)))
571+
self.assertEqual(asyncio.run(iterate(mock_instance)), expected)
523572

524-
with self.subTest("iterate through set return_value iterator"):
525-
mock_instance = MagicMock(self.WithAsyncIterator())
573+
def test_set_return_value_iter(mock_type):
574+
mock_instance = mock_type(self.WithAsyncIterator())
526575
mock_instance.__aiter__.return_value = iter(expected[:])
527-
self.assertEqual(expected, asyncio.run(iterate(mock_instance)))
576+
self.assertEqual(asyncio.run(iterate(mock_instance)), expected)
577+
578+
for mock_type in [AsyncMock, MagicMock]:
579+
with self.subTest(f"default value with {mock_type}"):
580+
test_default(mock_type)
581+
582+
with self.subTest(f"set return_value with {mock_type}"):
583+
test_set_return_value(mock_type)
584+
585+
with self.subTest(f"set return_value iterator with {mock_type}"):
586+
test_set_return_value_iter(mock_type)
528587

529588

530589
class AsyncMockAssert(unittest.TestCase):

Lib/unittest/test/testmock/testmagicmethods.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import asyncio
12
import math
23
import unittest
34
import os
45
import sys
5-
from unittest.mock import Mock, MagicMock, _magics
6+
from unittest.mock import AsyncMock, Mock, MagicMock, _magics
67

78

89

@@ -271,6 +272,34 @@ def test_magic_mock_equality(self):
271272
self.assertEqual(mock != mock, False)
272273

273274

275+
# This should be fixed with issue38163
276+
@unittest.expectedFailure
277+
def test_asyncmock_defaults(self):
278+
mock = AsyncMock()
279+
self.assertEqual(int(mock), 1)
280+
self.assertEqual(complex(mock), 1j)
281+
self.assertEqual(float(mock), 1.0)
282+
self.assertNotIn(object(), mock)
283+
self.assertEqual(len(mock), 0)
284+
self.assertEqual(list(mock), [])
285+
self.assertEqual(hash(mock), object.__hash__(mock))
286+
self.assertEqual(str(mock), object.__str__(mock))
287+
self.assertTrue(bool(mock))
288+
self.assertEqual(round(mock), mock.__round__())
289+
self.assertEqual(math.trunc(mock), mock.__trunc__())
290+
self.assertEqual(math.floor(mock), mock.__floor__())
291+
self.assertEqual(math.ceil(mock), mock.__ceil__())
292+
self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__))
293+
self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__))
294+
self.assertIsInstance(mock.__aenter__, AsyncMock)
295+
self.assertIsInstance(mock.__aexit__, AsyncMock)
296+
297+
# in Python 3 oct and hex use __index__
298+
# so these tests are for __index__ in py3k
299+
self.assertEqual(oct(mock), '0o1')
300+
self.assertEqual(hex(mock), '0x1')
301+
# how to test __sizeof__ ?
302+
274303
def test_magicmock_defaults(self):
275304
mock = MagicMock()
276305
self.assertEqual(int(mock), 1)
@@ -286,6 +315,10 @@ def test_magicmock_defaults(self):
286315
self.assertEqual(math.trunc(mock), mock.__trunc__())
287316
self.assertEqual(math.floor(mock), mock.__floor__())
288317
self.assertEqual(math.ceil(mock), mock.__ceil__())
318+
self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__))
319+
self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__))
320+
self.assertIsInstance(mock.__aenter__, AsyncMock)
321+
self.assertIsInstance(mock.__aexit__, AsyncMock)
289322

290323
# in Python 3 oct and hex use __index__
291324
# so these tests are for __index__ in py3k
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixes AsyncMock so it doesn't crash when used with AsyncContextManagers
2+
or AsyncIterators.

0 commit comments

Comments
 (0)