Comment mocker une coroutine ?


Dans le guide sur les tests en python (que je dois toujours terminer, je sais…), je vous parle des objets mocks.

Si vous avez eu le plaisir de jouer avec asyncio, vous avez du noter que unittest.mock n’a aucun outil pour gérer gérer les coroutines.

En attendant que ce soit intégré à la stdlib, voici une petite recette :

import asyncio
from unittest.mock import Mock
 
# on utilise toute la machinerie du Mock original
class AMock(Mock):
 
 
    def __call__(self, *args, **kwargs):
        # la référence du parent doit se récupérer hors
        # hors de la closure
        parent = super(AMock, self) 
        # sauf qu'à l'appel on créé une fonction coroutine
        @asyncio.coroutine
        def coro():
            # Qui fait le vrai Mock.__call__ (et donc popule l'historique 
            # des appels), mais seulement après l'évent loop l'ait éxécuté
            return parent.__call__(*args, **kwargs)
 
        # On appelle la fonction coroutine pour générer une coroutine
        # (les coroutines marchent comme les générateurs)
        return coro()

Je propose qu’en l’honneur de ce bidouillage, on l’appelle… mockoroutine !

Ca va s’utiliser comme ça:

mockorourine = AMock()
yield from mockorourine()

Après le yield from, mockorourine.call_count == 1, et mockorourine.assert_called_once_with()passe.

Si vous êtes en 3.5+, on peut même faire:

class AMock(Mock):
 
    def __call__(self, *args, **kwargs):
        parent = super(AMock, self)
        async def coro():
            return parent.__call__(*args, **kwargs)
        return coro()
 
    def __await__(self):
        # on delegue le await à la couroutine créée par __call__
        return self().__await__()

Puis:

await AMock()

6 thoughts on “Comment mocker une coroutine ?

  • Martius

    J’écris une bibliothèque qui s’appelle asynctest (aussi sur github et Pypi).

    Elle surcharge le paquet standard unittest pour ajouter des fonctionnalités permettant de tester du code asyncio. Elle inclue des outils pour facilement faire des mocks de coroutines (y compris avec de l’auto-mocking ou patch()). Elle permet aussi d’utiliser directement des coroutines dans les fonctions de setup, teardown, test ou cleanup (ce qui fait économiser pas mal de boilerplate).

  • Sam Post author

    Hi. It’s cool but I don’t get how it helps me:

        >>> loop = asyncio.get_event_loop()
        >>> async def foo():
        ...     b = await CoroutineMock()
        ...     print(b)
     
        >>> loop.run_until_complete(foo())
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
          File "/usr/lib/python3.5/asyncio/base_events.py", line 337, in run_until_complete
            return future.result()
          File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result
            raise self._exception
          File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
            result = coro.send(None)
          File "<stdin>", line 2, in foo
        TypeError: object CoroutineMock can't be used in 'await' expression
        object CoroutineMock can't be used in 'await' expression
     
        >>> async def foo():
        ...     b = await asyncio.ensure_future(CoroutineMock())
        ...   
        ...     print(b)
     
        >>> loop.run_until_complete(foo())
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
          File "/usr/lib/python3.5/asyncio/base_events.py", line 337, in run_until_complete
            return future.result()
          File "/usr/lib/python3.5/asyncio/futures.py", line 274, in result
            raise self._exception
          File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
            result = coro.send(None)
          File "<stdin>", line 2, in foo
          File "/usr/lib/python3.5/asyncio/tasks.py", line 546, in ensure_future
            raise TypeError('A Future, a coroutine or an awaitable is required')
        TypeError: A Future, a coroutine or an awaitable is required
        A Future, a coroutine or an awaitable is required

    If I can’t use it anywhere I would use a regular coroutine, what’s the point of mocking it ?

    Did I use it wrong ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.