Mocking an Async Property in Python

Question:

The below example class has a property bar that is awaitable, as in async_main() because it (theoretically) does some IO work before returning the answer to everything.

class Foo:

    @property
    async def bar(self):
        return 42


async def async_main():
    f = Foo()
    print(await f.bar)

I’m having trouble testing it, as the usual suspects of Mock, MagicMock, and AsyncMock don’t work with properties as expected. My current workaround is:

f.bar = some_awaitable()

since this makes f.bar a ‘field’ that can be awaited, but unfortunately I need to access it multiple times while it’s under test, which yields RuntimeError: cannot reuse already awaited coroutine on the second access of course.

Is there an established way to mock an async property like this?

Asked By: Brendano257

||

Answers:

The easiest way that I can think of is to patch bar again with an async property for the purposes of your test.

I am assuming you have some other method on Foo that you want to test, and that method calls its bar.


code.py

from asyncio import run


class Foo:
    @property
    async def bar(self) -> int:
        return 42

    async def func(self) -> int:
        return await self.bar


async def main():
    f = Foo()
    print(await f.func())


if __name__ == '__main__':
    run(main())

test.py

from unittest import IsolatedAsyncioTestCase
from unittest.mock import patch

from . import code


class FooTestCase(IsolatedAsyncioTestCase):
    async def test_func(self) -> None:
        expected_output = 69420

        @property
        async def mock_bar(_foo_self: code.Foo) -> int:
            return expected_output

        with patch.object(code.Foo, "bar", new=mock_bar):
            f = code.Foo()
            # Just to see that our mocking worked:
            self.assertEqual(expected_output, await f.bar)
            # Should call `bar` property again:
            output = await f.func()
        self.assertEqual(expected_output, output)

References: patch docs.

Answered By: Daniil Fajnberg

Property Foo.bar does not have setter so the instance of class Foo cannot assign a mock object to that property. The appropriate solution is to patch Foo.bar with PropertyMock.

import asyncio
from unittest import mock


async def test_bar():
    expected_value = 'expected value'

    async_class_mock = mock.AsyncMock(**{'coroutine.return_value': expected_value})
    task = asyncio.create_task(async_class_mock.coroutine())
    patch_property_with = mock.PropertyMock(spec=Foo, **{'bar': task})

    with mock.patch.object(target=Foo, attribute='bar', new=patch_property_with) as bar_mock:
        Foo.bar = bar_mock.bar
        assert await Foo().bar == expected_value

Here’s what’s happening:

  1. async_class_mock is an instance of AsyncMock which has been initialized with a pseudo coroutine whose return value is set to the value which the test expects from Foo.bar to return.
>>> async_class_mock.coroutine()
<coroutine object AsyncMockMixin._execute_mock_call at 0x00>
>>> await async_class_mock.coroutine()
'expected value'
  1. asyncio.create_task will schedule the execution of async_class_mock.coroutine() when the task will be awaited. Because it is a task, you can await it multiple times.
>>> task
<Task pending name='Task-1' coro=<AsyncMockMixin._execute_mock_call() running at ...>>
>>> await task
'expected value'
>>> await task
'expected value'
  1. patch_property_with is an instance of PropertyMock which is being assigned to Foo.bar. It is redefining what Foo.bar is about.
>>> bar_mock
<PropertyMock spec='Foo' ...>
>>> bar_mock.bar
<Task pending name='Task-1' coro=<AsyncMockMixin._execute_mock_call() running at ...>>

>>> Foo.bar = bar_mock.bar

Now Foo.bar references to an asyncio task, it can be awaited multiple times to get the expected value.

>>> foo = Foo()
>>> await foo.bar
'expected value'
>>> await foo.bar == expected_value
True

Once the context manager exits, patches applied to Foo.bar will be removed.

Answered By: defalt