Reputation: 1172
When I was trying to mock an async function in unittest with MagicMock, I got this exception:
TypeError: object MagicMock can't be used in 'await' expression
With sample code like:
# source code
class Service:
async def compute(self, x):
return x
class App:
def __init__(self):
self.service = Service()
async def handle(self, x):
return await self.service.compute(x)
# test code
import asyncio
import unittest
from unittest.mock import patch
class TestApp(unittest.TestCase):
@patch('__main__.Service')
def test_handle(self, mock):
loop = asyncio.get_event_loop()
app = App()
res = loop.run_until_complete(app.handle('foo'))
app.service.compute.assert_called_with("foo")
if __name__ == '__main__':
unittest.main()
How should I fix it with built-in python3 libraries?
Upvotes: 55
Views: 54061
Reputation: 1501
I came upon this SO discussion when I encountered errors like the following in my own code/test code:
TypeError: object MagicMock can't be used in 'await' expression
TypeError: object NonCallableMagicMock can't be used in 'await' expression
TypeError: object AsyncMock can't be used in 'await' expression
My code under test was asynchronous Django ORM queries where I wanted to mock the ultimate return value of some DB lookup, e.g.:
try:
user = await AllauthUser.objects.aget(email=email)
except AllauthUser.DoesNotExist:
# handle user not found
Ultimately, I needed to simply add the following kwarg to my mock object initialization: new_callable=AsyncMock
. Docs can be seen here.
I'm sharing this answer because I spent more time than I'd care to admit banging my head against these type errors!
Upvotes: 0
Reputation: 59
If you'd like a pytest-mock
solution compatible with < py3.8, I did something like this.
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super().__call__(*args, **kwargs)
def test_my_method(mocker):
my_mock = mocker.patch("path.to.mocked.thing", AsyncMock())
my_mock.return_value = [1, 2, 3]
assert my_method()
Definitely borrowed from Tomasz's solution here!
Upvotes: 1
Reputation: 1803
To override async classes one needs to tell patch
that the return_value
needs to be AsyncMock
. So use
@patch('__main__.Service', return_value=AsyncMock(Service))
def test_handle(self, mock):
loop = asyncio.get_event_loop()
app = App()
res = loop.run_until_complete(app.handle('foo'))
app.service.compute.assert_called_with("foo")
With that Service
will be a MagicMock
, but Service()
will return an AsyncMock
instance of Service
.
Upvotes: 0
Reputation: 14988
I found this comment very useful when trying to await
a mock object in Python < 3.8. You simply create a child class AsyncMock
that inherits from MagicMock
and overwrites a __call__
method to be a coroutine:
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
Then, inside your test, do:
@pytest.mark.asyncio
async def test_my_method():
# Test "my_method" coroutine by injecting an async mock
my_mock = AsyncMock()
assert await my_method(my_mock)
you might also want to install pytest-asyncio
Upvotes: 6
Reputation: 3539
In python 3.8+ you can make use of the AsyncMock
async def test_that_mock_can_be_awaited():
mock = AsyncMock()
mock.x.return_value = 123
result = await mock.x()
assert result == 123
The class AsyncMock
object will behave so the object is recognized as an async function, and the result of a call is an awaitable.
>>> mock = AsyncMock()
>>> asyncio.iscoroutinefunction(mock)
True
>>> inspect.isawaitable(mock())
True
Upvotes: 41
Reputation: 307
shaun shia provided really good universal solution, but i found what in python 3.8 you can use just @patch('__main__.Service', new=AsyncMock)
Upvotes: 14
Reputation: 8585
You can get mocks to return objects that can be awaited by using a Future. The following is a pytest test case, but something similar should be possible with unittest.
async def test_that_mock_can_be_awaited():
mock = MagicMock(return_value=Future())
mock.return_value.set_result(123)
result = await mock()
assert result == 123
In your case, since you're patching Service
(which gets passed in as mock
), mock.return_value = Future()
should do the trick.
Upvotes: 13
Reputation: 1172
I ended up with this hack.
# monkey patch MagicMock
async def async_magic():
pass
MagicMock.__await__ = lambda x: async_magic().__await__()
It only works for MagicMock, not other pre-defined return_value
Upvotes: 28