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?
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.
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:
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'
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'
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.
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?
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.
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:
async_class_mock
is an instance ofAsyncMock
which has been initialized with a pseudo coroutine whose return value is set to the value which the test expects fromFoo.bar
to return.
>>> async_class_mock.coroutine()
<coroutine object AsyncMockMixin._execute_mock_call at 0x00>
>>> await async_class_mock.coroutine()
'expected value'
asyncio.create_task
will schedule the execution ofasync_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'
patch_property_with
is an instance ofPropertyMock
which is being assigned toFoo.bar
. It is redefining whatFoo.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.