Comment simuler un appel asynchrone d'une coroutine native à une autre en utilisant unittest.mock.patch
?
J'ai actuellement une solution assez délicate:
class CoroutineMock(MagicMock):
def __await__(self, *args, **kwargs):
future = Future()
future.set_result(self)
result = yield from future
return result
Alors
class TestCoroutines(TestCase):
@patch('some.path', new_callable=CoroutineMock)
def test(self, mock):
some_action()
mock.assert_called_with(1,2,3)
Cela fonctionne mais semble moche. Y a-t-il plus de façon Pythonique de le faire?
Le sous-classement MagicMock
propagera votre classe personnalisée pour tous les mocks générés à partir de votre mock coroutine. Par exemple, AsyncMock().__str__
deviendra également un AsyncMock
ce qui n'est probablement pas ce que vous recherchez.
Au lieu de cela, vous souhaiterez peut-être définir une fabrique qui crée un Mock
(ou un MagicMock
) avec des arguments personnalisés, par exemple side_effect=coroutine(coro)
. En outre, il peut être judicieux de séparer la fonction coroutine de la coroutine (comme expliqué dans la documentation ).
Voici ce que j'ai trouvé:
from asyncio import coroutine
def CoroMock():
coro = Mock(name="CoroutineResult")
corofunc = Mock(name="CoroutineFunction", side_effect=coroutine(coro))
corofunc.coro = coro
return corofunc
Une explication des différents objets:
corofunc
: la maquette de la fonction coroutinecorofunc.side_effect()
: la coroutine, générée pour chaque appelcorofunc.coro
: La maquette utilisée par la coroutine pour obtenir le résultatcorofunc.coro.return_value
: La valeur retournée par la coroutinecorofunc.coro.side_effect
: Peut être utilisé pour déclencher une exceptionExemple:
async def coro(a, b):
return await sleep(1, result=a+b)
def some_action(a, b):
return get_event_loop().run_until_complete(coro(a, b))
@patch('__main__.coro', new_callable=CoroMock)
def test(corofunc):
a, b, c = 1, 2, 3
corofunc.coro.return_value = c
result = some_action(a, b)
corofunc.assert_called_with(a, b)
assert result == c
Tout le monde manque ce qui est probablement la solution la plus simple et la plus claire:
@patch('some.path')
def test(self, mock):
f = asyncio.Future()
f.set_result('whatever result you want')
mock.return_value = f
mock.assert_called_with(1, 2, 3)
rappelez-vous qu'une coroutine peut être considérée comme une simple fonction qui garantit le retour d'un avenir qui peut à son tour être attendu.
La solution était en fait assez simple: j'avais juste besoin de convertir __call__
méthode de simulation en coroutine:
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
Cela fonctionne parfaitement, lorsque mock est appelé, le code reçoit la coroutine native
Exemple d'utilisation:
@mock.patch('my.path.asyncio.sleep', new_callable=AsyncMock)
def test_stuff(sleep):
# code
Sur la base de la réponse @scolvin, j'ai créé cette méthode (imo) plus propre:
def async_return(result):
f = asyncio.Future()
f.set_result(result)
return f
C'est tout, utilisez-le autour du retour que vous voulez être asynchrone, comme dans
mock = MagicMock(return_value=async_return("Example return"))
await mock()
Une autre façon de se moquer de la coroutine est de faire de la coroutine, qui retourne en se moquant. De cette façon, vous pouvez vous moquer des coroutines qui seront passées dans asyncio.wait
ou asyncio.wait_for
.
Cela rend les coroutines plus universelles mais rend la configuration des tests plus lourde:
def make_coroutine(mock)
async def coroutine(*args, **kwargs):
return mock(*args, **kwargs)
return coroutine
class Test(TestCase):
def setUp(self):
self.coroutine_mock = Mock()
self.patcher = patch('some.coroutine',
new=make_coroutine(self.coroutine_mock))
self.patcher.start()
def tearDown(self):
self.patcher.stop()
Une autre variante de la solution "la plus simple" pour se moquer d'un objet asynchrone, qui est juste une doublure.
Dans la source:
class Yo:
async def foo(self):
await self.bar()
async def bar(self):
# Some code
En test:
from asyncio import coroutine
yo = Yo()
# Here bounded method bar is mocked and will return a customised result.
yo.bar = Mock(side_effect=coroutine(lambda:'the awaitable should return this'))
event_loop.run_until_complete(yo.foo())