web-dev-qa-db-fra.com

Comment se moquer des coroutines asyncologiques?

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())
17
Dustin Wyatt

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()
12
Andrew Svetlov

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)
12
Dustin Wyatt

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.).

9
Martin Richard

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__()
3
e-satis

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:

  1. Cela ne permet pas de lever facilement des exceptions (par exemple, en retournant certaines données, puis en générant une erreur lors de la seconde lecture).
  2. Je n'ai pas trouvé le moyen d'utiliser les attributs standard Mock.side_effect ou .return_value pour le rendre plus évident et lisible.
2
AlexandreH

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
0
Mark E. Haase