Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to patch an asynchronous class method?

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?

like image 888
Val Melev Avatar asked Jun 22 '18 16:06

Val Melev


1 Answers

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')]
like image 163
Martijn Pieters Avatar answered Oct 08 '22 20:10

Martijn Pieters