I'm working on the following problem, I have a class, an asynchronous method of which I want to mock patch:
class ExampleClass:
async def asy_method(self, param):
return await some_coroutine(self, param)
example_instance = ExampleClass()
I want to patch specifically only calls like
await example_instance.asy_method('test_param')
Normally I'd use
mocker.patch('ExampleClass.asy_method', new_callable=AsyncMock)
where mocker is the pytest-mock plugin fixture and AsyncMock has the form
class AsyncMock(mock.MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
which would give me a Mock object that behaves like a coroutine on call.
The problem is, that I want to have access to the self
attribute that is passed to the method. self
is only passed to the mock object if you set
autospec=True
though (see also Python Doc on patching unbound methods), which you can't use together with new_callable
.
Does anyone have an idea how to resolve this?
Indeed, you can't mix autospeccing and a new callable. Instead, autospec the method, but then replace the side_effect
attribute, giving it an AsyncMock()
instance:
from unittest import mock
def configure_coroutine_mock(mock_function, klass=AsyncMock):
"""Make an autospecced async function return a coroutine mock"""
mock_function.side_effect = AsyncMock()
# mark the side effect as a child of the original mock object
# so transitive access is recorded on the parent mock too. This is
# what .return_value does normally
mock._check_and_set_parent(
mock_function.mock, mock_function.side_effect,
None, '()')
return mock_asy_method.side_effect
with mocker.patch('ExampleClass.asy_method', autospec=True) as mock_asy_method:
configure_coroutine_mock(mock_asy_method)
Because the AsyncMock()
is a callable object, it'll be called every time mock_asy_method
is called, and the arguments are passed on to the object. The result of that call is then used to return from mock_asy_method()
:
>>> from unittest import mock
>>> class ExampleClass:
... async def asy_method(self, param):
... return await some_coroutine(self, param)
...
>>> example_instance = ExampleClass()
>>> with mock.patch('__main__.ExampleClass.asy_method', autospec=True) as mock_asy_method:
... configure_coroutine_mock(mock_asy_method)
... print(example_instance.asy_method('foo')) # call to patched class coroutine
... print(mock_asy_method.mock_calls) # calls are recorded
...
<AsyncMock name='asy_method()' id='4563887496'>
<coroutine object AsyncMock.__call__ at 0x1100780f8>
[call(<__main__.ExampleClass object at 0x10ffac1d0>, 'foo')]
As you can see, the self
argument and the parameter are recorded in the call, because mock_asy_method
is a properly specced function.
Of course, only if the returned AsyncMock()
call result is actually awaited will we see that call recorded too:
>>> with mock.patch('__main__.ExampleClass.asy_method', autospec=True) as mock_asy_method:
... configure_coroutine_mock(mock_asy_method)
... loop = asyncio.get_event_loop()
... coro = example_instance.asy_method('foo')
... loop.run_until_complete(coro)
... print(mock_asy_method.mock_calls)
...
<AsyncMock name='asy_method()' id='4564408920'>
<AsyncMock name='asy_method()()' id='4564999360'>
[call(<__main__.ExampleClass object at 0x10ffac1d0>, 'foo'),
call()(<__main__.ExampleClass object at 0x10ffac1d0>, 'foo')]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With