How to mock asyncio coroutines?
Solution 1
Since mock
library doesn't support coroutines I create mocked coroutines manually and assign those to mock object. A bit more verbose but it works.
Your example may look like this:
import asyncio
import unittest
from unittest.mock import Mock
class ImGoingToBeMocked:
@asyncio.coroutine
def yeah_im_not_going_to_run(self):
yield from asyncio.sleep(1)
return "sup"
class ImBeingTested:
def __init__(self, hidude):
self.hidude = hidude
@asyncio.coroutine
def i_call_other_coroutines(self):
return (yield from self.hidude.yeah_im_not_going_to_run())
class TestImBeingTested(unittest.TestCase):
def test_i_call_other_coroutines(self):
mocked = Mock(ImGoingToBeMocked)
ibt = ImBeingTested(mocked)
@asyncio.coroutine
def mock_coro():
return "sup"
mocked.yeah_im_not_going_to_run = mock_coro
ret = asyncio.get_event_loop().run_until_complete(
ibt.i_call_other_coroutines())
self.assertEqual("sup", ret)
if __name__ == '__main__':
unittest.main()
Solution 2
I am writting a wrapper to unittest which aims at cutting the boilerplate when writting tests for asyncio.
The code lives here: https://github.com/Martiusweb/asynctest
You can mock a coroutine with asynctest.CoroutineMock
:
>>> mock = CoroutineMock(return_value='a result')
>>> asyncio.iscoroutinefunction(mock)
True
>>> asyncio.iscoroutine(mock())
True
>>> asyncio.run_until_complete(mock())
'a result'
It also works with the side_effect
attribute, and an asynctest.Mock
with a spec
can return CoroutineMock:
>>> asyncio.iscoroutinefunction(Foo().coroutine)
True
>>> asyncio.iscoroutinefunction(Foo().function)
False
>>> asynctest.Mock(spec=Foo()).coroutine
<class 'asynctest.mock.CoroutineMock'>
>>> asynctest.Mock(spec=Foo()).function
<class 'asynctest.mock.Mock'>
All the features of unittest.Mock are expected to work correctly (patch(), etc).
Solution 3
Springing off of Andrew Svetlov's answer, I just wanted to share this helper function:
def get_mock_coro(return_value):
@asyncio.coroutine
def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
This lets you use the standard assert_called_with
, call_count
and other methods and attributes a regular unittest.Mock gives you.
You can use this with code in the question like:
class ImGoingToBeMocked:
@asyncio.coroutine
def yeah_im_not_going_to_run(self):
yield from asyncio.sleep(1)
return "sup"
class ImBeingTested:
def __init__(self, hidude):
self.hidude = hidude
@asyncio.coroutine
def i_call_other_coroutines(self):
return (yield from self.hidude.yeah_im_not_going_to_run())
class TestImBeingTested(unittest.TestCase):
def test_i_call_other_coroutines(self):
mocked = Mock(ImGoingToBeMocked)
mocked.yeah_im_not_going_to_run = get_mock_coro()
ibt = ImBeingTested(mocked)
ret = asyncio.get_event_loop().run_until_complete(ibt.i_call_other_coroutines())
self.assertEqual(mocked.yeah_im_not_going_to_run.call_count, 1)
Solution 4
You can create asynchronous mocks yourself:
import asyncio
from unittest.mock import Mock
class AsyncMock(Mock):
def __call__(self, *args, **kwargs):
sup = super(AsyncMock, self)
async def coro():
return sup.__call__(*args, **kwargs)
return coro()
def __await__(self):
return self().__await__()
Solution 5
A slightly simplified example for python 3.6+ adapted from a few of the answers here:
import unittest
class MyUnittest()
# your standard unittest function
def test_myunittest(self):
# define a local mock async function that does what you want, such as throw an exception. The signature should match the function you're mocking.
async def mock_myasync_function():
raise Exception('I am testing an exception within a coroutine here, do what you want')
# patch the original function `myasync_function` with the one you just defined above, note the usage of `wrap`, which hasn't been used in other answers.
with unittest.mock.patch('mymodule.MyClass.myasync_function', wraps=mock_myasync_function) as mock:
with self.assertRaises(Exception):
# call some complicated code that ultimately schedules your asyncio corotine mymodule.MyClass.myasync_function
do_something_to_call_myasync_function()
Related videos on Youtube
Dustin Wyatt
Updated on July 10, 2022Comments
-
Dustin Wyatt almost 2 years
The following code fails with
TypeError: 'Mock' object is not iterable
inImBeingTested.i_call_other_coroutines
because I've replacedImGoingToBeMocked
by a Mock object.How can I mock coroutines?
class ImGoingToBeMocked: @asyncio.coroutine def yeah_im_not_going_to_run(self): yield from asyncio.sleep(1) return "sup" class ImBeingTested: def __init__(self, hidude): self.hidude = hidude @asyncio.coroutine def i_call_other_coroutines(self): return (yield from self.hidude.yeah_im_not_going_to_run()) class TestImBeingTested(unittest.TestCase): def test_i_call_other_coroutines(self): mocked = Mock(ImGoingToBeMocked) ibt = ImBeingTested(mocked) ret = asyncio.get_event_loop().run_until_complete(ibt.i_call_other_coroutines())
-
Dustin Wyatt about 9 yearsWell this is obvious and now I feel dumb for asking the question! Thanks!
-
Dustin Wyatt about 9 yearsI expanded this with with a helper and stuff in this answer: stackoverflow.com/a/29905620/23972
-
AlexandreH about 9 years+1 for the
wraps
keyword, which did make me understand its purpose a bit more. Quick follow up question; how would you go about returning more than one value from a mocked coroutine? i.e.read()
is a coroutine and you want to first return some datab'data'
, and then return an EOF-like condition (e.g. no data,b''
orNone
). AFAICS you can't use.return_value
or.side_effect
on the mocked coroutine (gives abad yield
error). -
Natim about 5 yearsI tried that, but I got:
ValueError: a coroutine was expected, got <_GatheringFuture pending>
while using it inasyncio.gather(AsyncMock())
-
Wowbagger and his liquid lunch about 5 years
AsyncMock
acts like a coroutine function, butgather
expects a coroutine. You probably need an extra set of parentheses, likeasyncio.gather(AsyncMock()())
. -
Natim about 5 yearsWhat about
from asynctest import CoroutineMock
-
Natim about 5 yearsWith an extra set of parenthesis I get
TypeError: 'generator' object is not callable
-
Natim about 5 yearsI put a reproducible code here: github.com/Martiusweb/asynctest/issues/114
-
Natim about 5 yearsI found out, that, for some reasons, it is crashing with
asyncio.run
while working withloop.run_until_complete
— bugs.python.org/issue36222 -
Natim about 5 yearsAll this is because gather returns a future not a coroutine.
-
calvin over 3 years