Le code suivant échoue avec TypeError: 'Mock' object is not iterable
dans ImBeingTested.i_call_other_coroutines
car j'ai remplacé ImGoingToBeMocked
par un objet factice.
Comment puis-je me moquer des 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())
Puisque la bibliothèque mock
ne prend pas en charge les coroutines, je crée manuellement des coroutines moquées et les affecte à un objet fictif. Un peu plus verbeux mais ça marche.
Votre exemple peut ressembler à ceci:
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 __== '__main__':
unittest.main()
Jaillissant de la réponse de Andrew Svetlov , je voulais juste partager cette fonction d'assistance:
def get_mock_coro(return_value):
@asyncio.coroutine
def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
Cela vous permet d'utiliser le assert_called_with
, le call_count
et les autres méthodes et attributs standard qu'ununest.Mock vous donne.
Vous pouvez utiliser ceci avec du code dans la question comme:
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)
Je suis en train d’écrire une page d’emballage qui vise à couper le passe-partout lors de la rédaction des tests pour l’asyncio.
Le code vit ici: https://github.com/Martiusweb/asynctest
Vous pouvez vous moquer d'une coroutine avec asynctest.CoroutineMock
:
>>> mock = CoroutineMock(return_value='a result')
>>> asyncio.iscoroutinefunction(mock)
True
>>> asyncio.iscoroutine(mock())
True
>>> asyncio.run_until_complete(mock())
'a result'
Cela fonctionne aussi avec l'attribut side_effect
, et un asynctest.Mock
avec une spec
peut renvoyer 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'>
Toutes les fonctionnalités de unittest.Mock devraient fonctionner correctement (patch (), etc.).
Vous pouvez créer vous-même des simulacres asynchrones:
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__()
La réponse de Dustin est probablement la bonne dans la grande majorité des cas. J'ai eu un problème différent où la coroutine devait renvoyer plus d'une valeur, par exemple. simulant une opération read()
, comme décrit brièvement dans mon commentaire .
Après quelques tests supplémentaires, le code ci-dessous a fonctionné pour moi, en définissant un itérateur en dehors de la fonction de moquage, en mémorisant efficacement la dernière valeur renvoyée pour l'envoi du suivant:
def test_some_read_operation(self):
#...
data = iter([b'data', b''])
@asyncio.coroutine
def read(*args):
return next(data)
mocked.read = Mock(wraps=read)
# Here, the business class would use its .read() method which
# would first read 4 bytes of data, and then no data
# on its second read.
Donc, en développant la réponse de Dustin, cela ressemblerait à ceci:
def get_mock_coro(return_values):
values = iter(return_values)
@asyncio.coroutine
def mock_coro(*args, **kwargs):
return next(values)
return Mock(wraps=mock_coro)
Les deux inconvénients immédiats que je peux voir dans cette approche sont les suivants:
Mock
.side_effect
ou .return_value
pour le rendre plus évident et lisible.Eh bien, il y a déjà beaucoup de réponses ici, mais je vais contribuer ma version développée de la réponse de e-satis . Cette classe simule une fonction asynchrone et suit le nombre d'appels et les arguments d'appel, comme le fait la classe Mock pour les fonctions de synchronisation.
Testé sur Python 3.7.0.
class AsyncMock:
''' A mock that acts like an async def function. '''
def __init__(self, return_value=None, return_values=None):
if return_values is not None:
self._return_value = return_values
self._index = 0
else:
self._return_value = return_value
self._index = None
self._call_count = 0
self._call_args = None
self._call_kwargs = None
@property
def call_args(self):
return self._call_args
@property
def call_kwargs(self):
return self._call_kwargs
@property
def called(self):
return self._call_count > 0
@property
def call_count(self):
return self._call_count
async def __call__(self, *args, **kwargs):
self._call_args = args
self._call_kwargs = kwargs
self._call_count += 1
if self._index is not None:
return_index = self._index
self._index += 1
return self._return_value[return_index]
else:
return self._return_value
Exemple d'utilisation:
async def test_async_mock():
foo = AsyncMock(return_values=(1,2,3))
assert await foo() == 1
assert await foo() == 2
assert await foo() == 3