web-dev-qa-db-fra.com

Appel asynchrone moqueur dans python 3.5

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?

30
Zozz

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 coroutine
  • corofunc.side_effect(): la coroutine, générée pour chaque appel
  • corofunc.coro: La maquette utilisée par la coroutine pour obtenir le résultat
  • corofunc.coro.return_value: La valeur retournée par la coroutine
  • corofunc.coro.side_effect: Peut être utilisé pour déclencher une exception

Exemple:

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
11
Vincent

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.

34
SColvin

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
33
Zozz

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()
6
Ivan Castellanos

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()
6
Zozz

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())
3
Murphy Meng